feat(tui): add transcript compaction progress marker

This commit is contained in:
Hypatia May
2026-07-31 11:46:43 +08:00
parent afe8d49691
commit 949b9fba8d
11 changed files with 115 additions and 26 deletions

View File

@@ -32,10 +32,14 @@ import { contentText, type ParsedArguments } from './content.ts'
import {
formatCompletionTime,
formatTimingTotals,
STATUS_ANIMATION_INTERVAL_MS,
stepTimingAt,
type StepPosition,
} from '../chat/timing.ts'
const COMPACTION_PROGRESS_FRAMES = ['◐', '◓', '◑', '◒'] as const
const COMPACTION_PROGRESS_LABEL = 'Compaction in progress…'
/** Concatenate the text of every block of one type, separated by blank lines. */
function textBlocks(content: readonly ContentBlock[], type: 'text' | 'reasoning'): string {
return content
@@ -120,6 +124,29 @@ export class HeaderComponent implements Component {
}
}
/**
* Process-local transcript tail announcing a live standalone compaction.
* The leading blank belongs to the component so removing it leaves no gap.
*/
export class CompactionProgressComponent implements Component {
constructor(
private readonly startedAt: number,
private readonly now: () => number,
private readonly palette: Palette,
) {}
invalidate(): void {}
render(width: number): string[] {
const elapsed = Math.max(0, this.now() - this.startedAt)
const frameIndex = Math.floor(elapsed / STATUS_ANIMATION_INTERVAL_MS)
% COMPACTION_PROGRESS_FRAMES.length
const frame = COMPACTION_PROGRESS_FRAMES[frameIndex] as string
const marker = `${this.palette.accent(frame)} ${this.palette.dim(COMPACTION_PROGRESS_LABEL)}`
return ['', truncateToWidth(marker, Math.max(1, width), '')]
}
}
/**
* A user or steering prompt in the transcript. An underlined accent role header
* plus blank-line spacing separate it from surrounding blocks; body lines carry

View File

@@ -94,6 +94,7 @@ import {
type Config,
} from './config.ts'
import {
CompactionProgressComponent,
ContextCardComponent,
type ToolCardVisibility,
HeaderComponent,
@@ -347,7 +348,11 @@ export function createTuiChat(
* Live standalone compaction observed by this process. Never derive this
* state from history: a resumed log may contain a stale orphaned start.
*/
let compacting: { startedAt: number; timer: ReturnType<typeof setInterval> } | undefined
let compacting: {
startedAt: number
timer: ReturnType<typeof setInterval>
progress: CompactionProgressComponent
} | undefined
// TUI steering submissions that the inbox has not yet claimed or discarded.
// Correlation ids avoid guessing whether a running-state submission actually
// joined steering or fell back to the queued-turn FIFO during turn close.
@@ -478,6 +483,11 @@ export function createTuiChat(
const requestRender = (): void => {
if (disposed) return
if (compacting !== undefined) {
compacting.progress.invalidate()
chat.removeChild(compacting.progress)
chat.addChild(compacting.progress)
}
updatePromptValues()
const inputPrompt = renderInputPrompt()
editor.setPrompt({ first: inputPrompt, continuation: ' '.repeat(visibleWidth(inputPrompt)) })
@@ -567,6 +577,7 @@ export function createTuiChat(
const clearStatus = (): void => {
if (compacting !== undefined) {
clearInterval(compacting.timer)
chat.removeChild(compacting.progress)
compacting = undefined
}
clearTurnStatus()
@@ -1525,9 +1536,11 @@ export function createTuiChat(
// a stale resumed orphan for current work.
if (event.type === 'compact/start' && event.data.turn === null) {
if (compacting === undefined) {
const startedAt = now()
compacting = {
startedAt: now(),
startedAt,
timer: setInterval(renderStatus, STATUS_ANIMATION_INTERVAL_MS),
progress: new CompactionProgressComponent(startedAt, now, palette),
}
runtime.terminal.setProgress(true)
}
@@ -1536,6 +1549,7 @@ export function createTuiChat(
}
if (event.type === 'compact/end' && event.data.turn === null && compacting !== undefined) {
clearInterval(compacting.timer)
chat.removeChild(compacting.progress)
compacting = undefined
if (event.data.error !== undefined) {
appendNotice(`Compaction failed: ${event.data.error}`, 'warning')