From c094ecd5ace745e3acea6c995239634d29f1a556 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Fri, 3 Jul 2026 15:37:48 +0800 Subject: [PATCH 1/4] feat(tui): show compaction summary with Ctrl-O --- .changeset/compaction-summary-toggle.md | 5 ++ .../src/tui/components/dialogs/compaction.ts | 48 ++++++++++++++++++- .../src/tui/components/dialogs/help-panel.ts | 2 +- .../tui/controllers/session-event-handler.ts | 6 ++- .../src/tui/controllers/session-replay.ts | 1 + .../src/tui/controllers/streaming-ui.ts | 4 +- apps/kimi-code/src/tui/kimi-tui.ts | 2 +- apps/kimi-code/src/tui/types.ts | 1 + .../tui/components/dialogs/compaction.test.ts | 42 ++++++++++++++++ .../test/tui/kimi-tui-message-flow.test.ts | 40 ++++++++++++++++ .../kimi-code/test/tui/message-replay.test.ts | 15 ++++-- 11 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 .changeset/compaction-summary-toggle.md diff --git a/.changeset/compaction-summary-toggle.md b/.changeset/compaction-summary-toggle.md new file mode 100644 index 000000000..376940493 --- /dev/null +++ b/.changeset/compaction-summary-toggle.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Show compaction summaries in the TUI after compaction. Press Ctrl-O to show or hide the summary. diff --git a/apps/kimi-code/src/tui/components/dialogs/compaction.ts b/apps/kimi-code/src/tui/components/dialogs/compaction.ts index a20b947a0..c34babdab 100644 --- a/apps/kimi-code/src/tui/components/dialogs/compaction.ts +++ b/apps/kimi-code/src/tui/components/dialogs/compaction.ts @@ -32,6 +32,9 @@ export class CompactionComponent extends Container { private canceled = false; private tokensBefore: number | undefined; private tokensAfter: number | undefined; + private summary: string | undefined; + private summaryText: Text | undefined; + private expanded = false; constructor(ui?: TUI, instruction?: string | undefined, tip?: string) { super(); @@ -70,13 +73,17 @@ export class CompactionComponent extends Container { super.invalidate(); } - markDone(tokensBefore?: number, tokensAfter?: number): void { + markDone(tokensBefore?: number, tokensAfter?: number, summary?: string): void { if (this.done || this.canceled) return; this.done = true; this.tokensBefore = tokensBefore; this.tokensAfter = tokensAfter; + this.summary = summary; this.stopBlink(); this.headerText.setText(this.buildHeader()); + if (this.expanded) { + this.addSummaryChild(); + } this.ui?.requestRender(); } @@ -88,6 +95,39 @@ export class CompactionComponent extends Container { this.ui?.requestRender(); } + setExpanded(expanded: boolean): void { + if (this.expanded === expanded) return; + this.expanded = expanded; + if (expanded) { + this.addSummaryChild(); + } else { + this.removeSummaryChild(); + } + this.headerText.setText(this.buildHeader()); + this.ui?.requestRender(); + } + + private addSummaryChild(): void { + if (this.summaryText !== undefined || this.summary === undefined || this.summary.length === 0) { + return; + } + const indentedSummary = this.summary + .split('\n') + .map((line) => ` ${line}`) + .join('\n'); + this.summaryText = new Text(currentTheme.dim(indentedSummary), 0, 0); + this.addChild(this.summaryText); + } + + private removeSummaryChild(): void { + if (this.summaryText === undefined) return; + const index = this.children.indexOf(this.summaryText); + if (index !== -1) { + this.children.splice(index, 1); + } + this.summaryText = undefined; + } + dispose(): void { this.stopBlink(); } @@ -100,7 +140,11 @@ export class CompactionComponent extends Container { this.tokensBefore !== undefined && this.tokensAfter !== undefined ? currentTheme.dim(` (${String(this.tokensBefore)} → ${String(this.tokensAfter)} tokens)`) : ''; - return `${bullet}${label}${detail}`; + const shortcutHint = + this.summary !== undefined && this.summary.length > 0 + ? currentTheme.dim(` (Ctrl-O to ${this.expanded ? 'hide' : 'show'} compaction summary)`) + : ''; + return `${bullet}${label}${detail}${shortcutHint}`; } if (this.canceled) { const bullet = currentTheme.fg('warning', STATUS_BULLET); diff --git a/apps/kimi-code/src/tui/components/dialogs/help-panel.ts b/apps/kimi-code/src/tui/components/dialogs/help-panel.ts index e1ecfa7e8..10fd5d5fe 100644 --- a/apps/kimi-code/src/tui/components/dialogs/help-panel.ts +++ b/apps/kimi-code/src/tui/components/dialogs/help-panel.ts @@ -33,7 +33,7 @@ export interface HelpPanelCommand { export const DEFAULT_KEYBOARD_SHORTCUTS: readonly KeyboardShortcut[] = [ { keys: 'Shift-Tab', description: 'Toggle plan mode' }, { keys: 'Ctrl-G', description: 'Edit in external editor ($VISUAL / $EDITOR)' }, - { keys: 'Ctrl-O', description: 'Toggle tool output expansion' }, + { keys: 'Ctrl-O', description: 'Toggle tool output / compaction summary expansion' }, { keys: 'Ctrl-T', description: 'Expand / collapse the todo list (when truncated)' }, { keys: 'Ctrl-S', description: 'Steer — inject a follow-up during streaming' }, { keys: 'Shift-Enter / Ctrl-J', description: 'Insert newline' }, diff --git a/apps/kimi-code/src/tui/controllers/session-event-handler.ts b/apps/kimi-code/src/tui/controllers/session-event-handler.ts index 7b73e7077..e389e1cbf 100644 --- a/apps/kimi-code/src/tui/controllers/session-event-handler.ts +++ b/apps/kimi-code/src/tui/controllers/session-event-handler.ts @@ -984,7 +984,11 @@ export class SessionEventHandler { event: CompactionCompletedEvent, sendQueued: (item: QueuedMessage) => void, ): void { - this.host.streamingUI.endCompaction(event.result.tokensBefore, event.result.tokensAfter); + this.host.streamingUI.endCompaction( + event.result.tokensBefore, + event.result.tokensAfter, + event.result.summary, + ); this.finishCompaction(sendQueued); } diff --git a/apps/kimi-code/src/tui/controllers/session-replay.ts b/apps/kimi-code/src/tui/controllers/session-replay.ts index b4f23bc48..00ee5a64b 100644 --- a/apps/kimi-code/src/tui/controllers/session-replay.ts +++ b/apps/kimi-code/src/tui/controllers/session-replay.ts @@ -472,6 +472,7 @@ export class SessionReplayRenderer { this.host.appendTranscriptEntry({ ...replayEntry(context, 'status', 'Compaction complete', 'plain'), compactionData: { + summary: record.result.summary, tokensBefore: record.result.tokensBefore, tokensAfter: record.result.tokensAfter, instruction: record.instruction, diff --git a/apps/kimi-code/src/tui/controllers/streaming-ui.ts b/apps/kimi-code/src/tui/controllers/streaming-ui.ts index 8658c7532..8d75810db 100644 --- a/apps/kimi-code/src/tui/controllers/streaming-ui.ts +++ b/apps/kimi-code/src/tui/controllers/streaming-ui.ts @@ -727,10 +727,10 @@ export class StreamingUIController { state.ui.requestRender(); } - endCompaction(tokensBefore?: number, tokensAfter?: number): void { + endCompaction(tokensBefore?: number, tokensAfter?: number, summary?: string): void { const block = this._activeCompactionBlock; if (block === undefined) return; - block.markDone(tokensBefore, tokensAfter); + block.markDone(tokensBefore, tokensAfter, summary); this._activeCompactionBlock = undefined; this.host.state.ui.requestRender(); } diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 7a423911f..8304525f2 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -1740,7 +1740,7 @@ export class KimiTUI { if (data.result === 'cancelled') { block.markCanceled(); } else { - block.markDone(data.tokensBefore, data.tokensAfter); + block.markDone(data.tokensBefore, data.tokensAfter, data.summary); } return block; } diff --git a/apps/kimi-code/src/tui/types.ts b/apps/kimi-code/src/tui/types.ts index a3c21e3fc..6dcdccdd1 100644 --- a/apps/kimi-code/src/tui/types.ts +++ b/apps/kimi-code/src/tui/types.ts @@ -119,6 +119,7 @@ export interface BackgroundAgentStatusData { export interface CompactionTranscriptData { readonly result?: 'cancelled'; + readonly summary?: string; readonly tokensBefore?: number; readonly tokensAfter?: number; readonly instruction?: string; diff --git a/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts b/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts index f4a9286f8..5e70d3a15 100644 --- a/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts +++ b/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts @@ -50,6 +50,7 @@ describe('CompactionComponent', () => { expect(text).toContain('Compaction complete'); expect(text).not.toContain('Tip:'); + expect(text).not.toContain('Ctrl-O'); } finally { component.dispose(); } @@ -70,6 +71,47 @@ describe('CompactionComponent', () => { } }); + it('keeps the completed compaction summary hidden until expanded', () => { + const component = new CompactionComponent(); + + try { + component.markDone(120, 24, 'Keep the src/tui compaction notes.'); + const collapsed = component.render(120).map(strip).join('\n'); + + expect(collapsed).toContain('Compaction complete'); + expect(collapsed).toContain('120 → 24 tokens'); + expect(collapsed).toContain('Ctrl-O to show compaction summary'); + expect(collapsed).not.toContain('Keep the src/tui compaction notes.'); + + component.setExpanded(true); + const expanded = component.render(120).map(strip).join('\n'); + + expect(expanded).toContain('Compaction complete'); + expect(expanded).toContain('Ctrl-O to hide compaction summary'); + expect(expanded).toContain('Keep the src/tui compaction notes.'); + } finally { + component.dispose(); + } + }); + + it('hides the compaction summary again when collapsed', () => { + const component = new CompactionComponent(); + + try { + component.markDone(120, 24, 'Keep the src/tui compaction notes.'); + component.setExpanded(true); + component.setExpanded(false); + const text = component.render(120).map(strip).join('\n'); + + expect(text).toContain('Compaction complete'); + expect(text).toContain('Ctrl-O to show compaction summary'); + expect(text).not.toContain('Ctrl-O to hide compaction summary'); + expect(text).not.toContain('Keep the src/tui compaction notes.'); + } finally { + component.dispose(); + } + }); + it('repaints the header with the active palette on invalidate', () => { // Force truecolor so palette differences surface as ANSI codes even when // the test runner has no TTY. diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index ae6a4b8a5..3fbfec50b 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -2023,6 +2023,46 @@ command = "vim" } }); + it('stores the live compaction summary and expands it with tool output expansion', async () => { + const { driver } = await makeDriver(); + const sendQueued = vi.fn(); + + driver.sessionEventHandler.handleEvent( + { + type: 'compaction.started', + agentId: 'main', + sessionId: 'ses-1', + trigger: 'manual', + } as Event, + sendQueued, + ); + + driver.sessionEventHandler.handleEvent( + { + type: 'compaction.completed', + agentId: 'main', + sessionId: 'ses-1', + result: { + summary: 'Keep the src/tui compaction notes.', + compactedCount: 4, + tokensBefore: 120, + tokensAfter: 24, + }, + } as Event, + sendQueued, + ); + + const collapsed = driver.state.transcriptContainer.render(120).map(stripSgr).join('\n'); + expect(collapsed).toContain('Compaction complete'); + expect(collapsed).not.toContain('Keep the src/tui compaction notes.'); + + driver.state.editor.onToggleToolExpand?.(); + + const expanded = driver.state.transcriptContainer.render(120).map(stripSgr).join('\n'); + expect(driver.state.toolOutputExpanded).toBe(true); + expect(expanded).toContain('Keep the src/tui compaction notes.'); + }); + it('renders an error instead of prompting when no model is selected', async () => { const { driver, session } = await makeDriver(); driver.state.appState.model = ''; diff --git a/apps/kimi-code/test/tui/message-replay.test.ts b/apps/kimi-code/test/tui/message-replay.test.ts index df92712d8..084c72432 100644 --- a/apps/kimi-code/test/tui/message-replay.test.ts +++ b/apps/kimi-code/test/tui/message-replay.test.ts @@ -1030,15 +1030,20 @@ describe('KimiTUI resume message replay', () => { (entry) => entry.compactionData !== undefined, ); expect(compactionEntry?.compactionData).toEqual({ + summary: 'Compacted transcript summary.', tokensBefore: 120, tokensAfter: 24, instruction: 'preserve implementation notes', }); - const transcript = stripAnsi(driver.state.transcriptContainer.render(120).join('\n')); - expect(transcript).toContain('Compaction complete'); - expect(transcript).toContain('120 → 24 tokens'); - expect(transcript).toContain('preserve implementation notes'); - expect(transcript).not.toContain('Compacted transcript summary.'); + const collapsed = stripAnsi(driver.state.transcriptContainer.render(120).join('\n')); + expect(collapsed).toContain('Compaction complete'); + expect(collapsed).toContain('120 → 24 tokens'); + expect(collapsed).toContain('preserve implementation notes'); + expect(collapsed).not.toContain('Compacted transcript summary.'); + + driver.state.editor.onToggleToolExpand?.(); + const expanded = stripAnsi(driver.state.transcriptContainer.render(120).join('\n')); + expect(expanded).toContain('Compacted transcript summary.'); }); it('renders replayed cancelled compaction records as cancelled compaction blocks', async () => { From 63d02f16bac51d36354b611d66bcf0607fc71947 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Fri, 3 Jul 2026 15:40:00 +0800 Subject: [PATCH 2/4] docs: document compaction summary toggle --- docs/en/guides/getting-started.md | 2 +- docs/en/guides/interaction.md | 2 +- docs/en/reference/keyboard.md | 4 ++-- docs/zh/guides/getting-started.md | 2 +- docs/zh/guides/interaction.md | 2 +- docs/zh/reference/keyboard.md | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/en/guides/getting-started.md b/docs/en/guides/getting-started.md index 7a1dddda4..2ff8f2a66 100644 --- a/docs/en/guides/getting-started.md +++ b/docs/en/guides/getting-started.md @@ -155,7 +155,7 @@ For a first-time user, the following is all you need to know: | `Ctrl-C` | Interrupt output; press twice while idle to exit | | `Shift-Tab` | Toggle Plan mode | | `Ctrl-S` | Inject a message mid-stream without waiting for the current response to finish | -| `Ctrl-O` | Collapse / expand tool output | +| `Ctrl-O` | Collapse / expand tool output and compaction summaries | For the full list, type `/help` or visit [Slash commands reference](../reference/slash-commands.md) and [Keyboard shortcuts](../reference/keyboard.md). diff --git a/docs/en/guides/interaction.md b/docs/en/guides/interaction.md index d905a8e12..c0433ab98 100644 --- a/docs/en/guides/interaction.md +++ b/docs/en/guides/interaction.md @@ -81,7 +81,7 @@ The input box remains usable while the agent is thinking or calling tools, and s - **`Ctrl-S`**: inject the content in the input box into the running turn immediately, without waiting for it to finish - **`Esc` / `Ctrl-C`**: interrupt the current turn -- **`Ctrl-O`**: globally toggle the collapsed/expanded state of tool output +- **`Ctrl-O`**: globally toggle the collapsed/expanded state of tool output and compaction summaries ## External editor diff --git a/docs/en/reference/keyboard.md b/docs/en/reference/keyboard.md index 3d7a52898..fb2ba76b0 100644 --- a/docs/en/reference/keyboard.md +++ b/docs/en/reference/keyboard.md @@ -67,9 +67,9 @@ Pressing `Ctrl-S` causes the model to see your message at the next interruptible | Shortcut | Function | | --- | --- | -| `Ctrl-O` | Expand or collapse tool output | +| `Ctrl-O` | Expand or collapse tool output and compaction summaries | -When collapsed tool call results exist in the history, press `Ctrl-O` to toggle between collapsed and expanded views. +When collapsed tool call results exist in the history, press `Ctrl-O` to toggle between collapsed and expanded views. After compaction, the same shortcut shows or hides the compaction summary in the compaction block. ## Approval Panel diff --git a/docs/zh/guides/getting-started.md b/docs/zh/guides/getting-started.md index c2e0e75e2..ca8ef87fc 100644 --- a/docs/zh/guides/getting-started.md +++ b/docs/zh/guides/getting-started.md @@ -155,7 +155,7 @@ Kimi Code CLI 会规划步骤、修改代码、运行测试,并在每一步告 | `Ctrl-C` | 中断输出;空闲时连按两次退出 | | `Shift-Tab` | 切换 Plan 模式 | | `Ctrl-S` | 输出中途插入消息,无需等待结束 | -| `Ctrl-O` | 折叠 / 展开工具输出 | +| `Ctrl-O` | 折叠 / 展开工具输出和压缩摘要 | 想看完整列表,输入 `/help` 或访问[斜杠命令参考](../reference/slash-commands.md)和[键盘快捷键](../reference/keyboard.md)。 diff --git a/docs/zh/guides/interaction.md b/docs/zh/guides/interaction.md index 66749766e..b2d22b175 100644 --- a/docs/zh/guides/interaction.md +++ b/docs/zh/guides/interaction.md @@ -81,7 +81,7 @@ Agent 思考或调用工具时,输入框仍然可用,支持以下额外操 - **`Ctrl-S`**:把输入框中的内容立即注入正在运行的轮次,无需等待结束 - **`Esc` / `Ctrl-C`**:中断当前轮次 -- **`Ctrl-O`**:全局切换工具输出的折叠状态 +- **`Ctrl-O`**:全局切换工具输出和压缩摘要的折叠状态 ## 外部编辑器 diff --git a/docs/zh/reference/keyboard.md b/docs/zh/reference/keyboard.md index 4a1c80c80..9e3c54a5a 100644 --- a/docs/zh/reference/keyboard.md +++ b/docs/zh/reference/keyboard.md @@ -67,9 +67,9 @@ Kimi Code CLI 的 TUI 交互模式支持一套键盘快捷键。键位按使用 | 快捷键 | 功能 | | --- | --- | -| `Ctrl-O` | 展开或折叠工具输出 | +| `Ctrl-O` | 展开或折叠工具输出和压缩摘要 | -历史中存在折叠的工具调用结果时,按 `Ctrl-O` 可在折叠和展开之间切换。 +历史中存在折叠的工具调用结果时,按 `Ctrl-O` 可在折叠和展开之间切换。压缩完成后,同一个快捷键也会在压缩块中显示或隐藏压缩摘要。 ## 审批面板 From 897da395e6a029c0b470abdae28cc05c58602c07 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Fri, 3 Jul 2026 16:29:43 +0800 Subject: [PATCH 3/4] fix(tui): preserve compaction summary expansion state --- .../src/tui/components/dialogs/compaction.ts | 23 +++++++----- .../src/tui/controllers/streaming-ui.ts | 3 ++ .../tui/components/dialogs/compaction.test.ts | 17 +++++++++ .../test/tui/kimi-tui-message-flow.test.ts | 37 +++++++++++++++++++ 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/apps/kimi-code/src/tui/components/dialogs/compaction.ts b/apps/kimi-code/src/tui/components/dialogs/compaction.ts index c34babdab..a0f0cf3ba 100644 --- a/apps/kimi-code/src/tui/components/dialogs/compaction.ts +++ b/apps/kimi-code/src/tui/components/dialogs/compaction.ts @@ -24,6 +24,7 @@ const BLINK_INTERVAL = 500; export class CompactionComponent extends Container { private readonly ui: TUI | undefined; private readonly headerText: Text; + private instructionText: Text | undefined; private readonly instruction: string | undefined; private readonly tip: string | undefined; private blinkOn = true; @@ -54,22 +55,26 @@ export class CompactionComponent extends Container { private addInstructionChild(): void { if (this.instruction !== undefined) { - this.addChild(new Text(currentTheme.dim(` ${this.instruction}`), 0, 0)); + this.instructionText = new Text(currentTheme.dim(` ${this.instruction}`), 0, 0); + this.addChild(this.instructionText); } } + private removeInstructionChild(): void { + if (this.instructionText === undefined) return; + const index = this.children.indexOf(this.instructionText); + if (index !== -1) { + this.children.splice(index, 1); + } + this.instructionText = undefined; + } + override invalidate(): void { // Repaint the header with the active palette (it caches ANSI codes). this.headerText.setText(this.buildHeader()); // Rebuild instruction line with fresh theme colours. - if (this.instruction !== undefined) { - // Remove the last child if it is the instruction line (it is always - // added after headerText and Spacer). - if (this.children.length > 2) { - this.children.pop(); - } - this.addInstructionChild(); - } + this.removeInstructionChild(); + this.addInstructionChild(); super.invalidate(); } diff --git a/apps/kimi-code/src/tui/controllers/streaming-ui.ts b/apps/kimi-code/src/tui/controllers/streaming-ui.ts index 8d75810db..bbfe25fdc 100644 --- a/apps/kimi-code/src/tui/controllers/streaming-ui.ts +++ b/apps/kimi-code/src/tui/controllers/streaming-ui.ts @@ -724,6 +724,9 @@ export class StreamingUIController { const block = new CompactionComponent(state.ui, instruction, currentWorkingTip()?.text); this._activeCompactionBlock = block; state.transcriptContainer.addChild(block); + if (state.toolOutputExpanded) { + block.setExpanded(true); + } state.ui.requestRender(); } diff --git a/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts b/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts index 5e70d3a15..3ac055172 100644 --- a/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts +++ b/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts @@ -112,6 +112,23 @@ describe('CompactionComponent', () => { } }); + it('preserves the expanded summary when invalidating with an instruction', () => { + const component = new CompactionComponent(undefined, 'keep the recent files only'); + + try { + component.markDone(120, 24, 'Keep the src/tui compaction notes.'); + component.setExpanded(true); + component.invalidate(); + const text = component.render(120).map(strip).join('\n'); + + expect(text).toContain('keep the recent files only'); + expect(text).toContain('Keep the src/tui compaction notes.'); + expect(text.match(/keep the recent files only/g)).toHaveLength(1); + } finally { + component.dispose(); + } + }); + it('repaints the header with the active palette on invalidate', () => { // Force truecolor so palette differences surface as ANSI codes even when // the test runner has no TTY. diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index 3fbfec50b..3503e4271 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -2063,6 +2063,43 @@ command = "vim" expect(expanded).toContain('Keep the src/tui compaction notes.'); }); + it('honors existing tool output expansion when a compaction block is created', async () => { + const { driver } = await makeDriver(); + const sendQueued = vi.fn(); + + driver.state.editor.onToggleToolExpand?.(); + expect(driver.state.toolOutputExpanded).toBe(true); + + driver.sessionEventHandler.handleEvent( + { + type: 'compaction.started', + agentId: 'main', + sessionId: 'ses-1', + trigger: 'manual', + } as Event, + sendQueued, + ); + + driver.sessionEventHandler.handleEvent( + { + type: 'compaction.completed', + agentId: 'main', + sessionId: 'ses-1', + result: { + summary: 'Keep the src/tui compaction notes.', + compactedCount: 4, + tokensBefore: 120, + tokensAfter: 24, + }, + } as Event, + sendQueued, + ); + + const transcript = driver.state.transcriptContainer.render(120).map(stripSgr).join('\n'); + expect(transcript).toContain('Compaction complete'); + expect(transcript).toContain('Keep the src/tui compaction notes.'); + }); + it('renders an error instead of prompting when no model is selected', async () => { const { driver, session } = await makeDriver(); driver.state.appState.model = ''; From 498c4fe4a1914e126b04130ff4c5cdaf5c5bab63 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Fri, 3 Jul 2026 17:01:23 +0800 Subject: [PATCH 4/4] fix(tui): preserve compaction summary expansion across replay and theme changes --- .../src/tui/components/dialogs/compaction.ts | 10 +++++++- apps/kimi-code/src/tui/kimi-tui.ts | 3 +++ .../tui/components/dialogs/compaction.test.ts | 19 +++++++++++++++ .../kimi-code/test/tui/message-replay.test.ts | 23 +++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/apps/kimi-code/src/tui/components/dialogs/compaction.ts b/apps/kimi-code/src/tui/components/dialogs/compaction.ts index a0f0cf3ba..9ade9350c 100644 --- a/apps/kimi-code/src/tui/components/dialogs/compaction.ts +++ b/apps/kimi-code/src/tui/components/dialogs/compaction.ts @@ -72,9 +72,17 @@ export class CompactionComponent extends Container { override invalidate(): void { // Repaint the header with the active palette (it caches ANSI codes). this.headerText.setText(this.buildHeader()); - // Rebuild instruction line with fresh theme colours. + // Rebuild instruction and summary text with fresh theme colours, preserving + // header → instruction → summary child order. + const expanded = this.expanded; this.removeInstructionChild(); + if (expanded) { + this.removeSummaryChild(); + } this.addInstructionChild(); + if (expanded) { + this.addSummaryChild(); + } super.invalidate(); } diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 8304525f2..8d182aa97 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -1741,6 +1741,9 @@ export class KimiTUI { block.markCanceled(); } else { block.markDone(data.tokensBefore, data.tokensAfter, data.summary); + if (this.state.toolOutputExpanded) { + block.setExpanded(true); + } } return block; } diff --git a/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts b/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts index 3ac055172..4f415bc32 100644 --- a/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts +++ b/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts @@ -129,6 +129,25 @@ describe('CompactionComponent', () => { } }); + it('keeps expanded summary child order on invalidate', () => { + const component = new CompactionComponent(undefined, 'keep the recent files only'); + + try { + component.markDone(120, 24, 'Keep the src/tui compaction notes.'); + component.setExpanded(true); + currentTheme.setPalette(lightColors); + component.invalidate(); + const text = component.render(120).map(strip).join('\n'); + + expect(text).toContain('Keep the src/tui compaction notes.'); + expect(text.indexOf('keep the recent files only')).toBeLessThan( + text.indexOf('Keep the src/tui compaction notes.'), + ); + } finally { + component.dispose(); + } + }); + it('repaints the header with the active palette on invalidate', () => { // Force truecolor so palette differences surface as ANSI codes even when // the test runner has no TTY. diff --git a/apps/kimi-code/test/tui/message-replay.test.ts b/apps/kimi-code/test/tui/message-replay.test.ts index 084c72432..5e4e11670 100644 --- a/apps/kimi-code/test/tui/message-replay.test.ts +++ b/apps/kimi-code/test/tui/message-replay.test.ts @@ -1046,6 +1046,29 @@ describe('KimiTUI resume message replay', () => { expect(expanded).toContain('Compacted transcript summary.'); }); + it('initializes replayed compaction blocks as expanded when tool output is already expanded', async () => { + const initial = makeSession([]); + const resumed = makeSession([ + { + time: REPLAY_TIME, + type: 'compaction', + result: { + summary: 'Compacted transcript summary.', + compactedCount: 4, + tokensBefore: 120, + tokensAfter: 24, + }, + }, + ]); + const driver = await makeDriver(initial); + driver.state.toolOutputExpanded = true; + await driver.switchToSession(resumed, 'Resumed session (ses-replay).'); + + const transcript = stripAnsi(driver.state.transcriptContainer.render(120).join('\n')); + expect(transcript).toContain('Compaction complete'); + expect(transcript).toContain('Compacted transcript summary.'); + }); + it('renders replayed cancelled compaction records as cancelled compaction blocks', async () => { const driver = await replayIntoDriver([ message('user', [{ type: 'text', text: 'prompt before cancellation' }]),