fix(tui): preserve compaction indicator ownership

This commit is contained in:
Hypatia May
2026-07-30 22:09:42 +08:00
parent bbff2c58de
commit 859d60fff5
2 changed files with 52 additions and 5 deletions

View File

@@ -1524,11 +1524,13 @@ export function createTuiChat(
// live durable bracket can announce its in-flight state without mistaking
// a stale resumed orphan for current work.
if (event.type === 'compact/start' && event.data.turn === null) {
compacting = {
startedAt: now(),
timer: setInterval(renderStatus, STATUS_ANIMATION_INTERVAL_MS),
if (compacting === undefined) {
compacting = {
startedAt: now(),
timer: setInterval(renderStatus, STATUS_ANIMATION_INTERVAL_MS),
}
runtime.terminal.setProgress(true)
}
runtime.terminal.setProgress(true)
requestRender()
return
}
@@ -1538,7 +1540,9 @@ export function createTuiChat(
if (event.data.error !== undefined) {
appendNotice(`Compaction failed: ${event.data.error}`, 'warning')
}
beginFadeOut(COMPACTING_GLYPH)
// A concurrently running turn owns the indicator. Keep its timer and
// progress bit instead of letting the compaction fade clear that state.
if (runningStatus === undefined) beginFadeOut(COMPACTING_GLYPH)
requestRender()
return
}

View File

@@ -2075,9 +2075,52 @@ describe('pi-tui chat lifecycle and transcript', () => {
expect(result.terminal.output).toContain('dsh ◍ ')
expect(result.terminal.output).not.toContain('dsh ⊙ ')
result.session.append('compact/end', { turn: null })
await tick()
result.terminal.output = ''
result.terminal.resize(result.terminal.columns + 1)
await tick()
expect(result.terminal.output).toContain('dsh ◍ ')
expect(result.terminal.output).not.toContain('dsh ⊙ ')
expect(result.terminal.progress.at(-1)).toBe(true)
await dispose(result)
})
it('treats duplicate live compaction starts as one owned bracket', async () => {
const intervalSpy = vi.spyOn(globalThis, 'setInterval')
const clearIntervalSpy = vi.spyOn(globalThis, 'clearInterval')
let result: Awaited<ReturnType<typeof setup>> | undefined
let didDispose = false
let clock = 0
try {
result = await setup({ omitInitialLifecycle: true, now: () => clock })
intervalSpy.mockClear()
clearIntervalSpy.mockClear()
result.session.append('compact/start', { turn: null })
clock = 1_000
result.terminal.output = ''
result.session.append('compact/start', { turn: null })
await tick()
expect(intervalSpy).toHaveBeenCalledOnce()
expect(result.terminal.output).toContain('dsh ⊙ ')
expect(result.terminal.progress.at(-1)).toBe(true)
result.session.append('compact/end', { turn: null })
await tick()
expect(clearIntervalSpy).toHaveBeenCalledOnce()
expect(result.terminal.progress.at(-1)).toBe(false)
await dispose(result)
didDispose = true
} finally {
if (result !== undefined && !didDispose) await dispose(result)
intervalSpy.mockRestore()
clearIntervalSpy.mockRestore()
}
})
it('does not show compaction progress for a resumed orphaned start', async () => {
const result = await setup({
omitInitialLifecycle: true,