diff --git a/.agents/skills/gen-changesets/SKILL.md b/.agents/skills/gen-changesets/SKILL.md index 949dfa22d..2fdc84040 100644 --- a/.agents/skills/gen-changesets/SKILL.md +++ b/.agents/skills/gen-changesets/SKILL.md @@ -44,10 +44,12 @@ Format: | Level | When to use | |---|---| -| `patch` | Bug fixes; build/package fixes; internal refactors that do not change behavior; wording tweaks; small dependency upgrades | -| `minor` | New backwards-compatible features or capabilities | +| `patch` | Bug fixes; build/package fixes; internal refactors that do not change behavior; wording tweaks; small dependency upgrades; small improvements to existing features with limited user-facing impact (e.g. a new keyboard shortcut, a flag alias, a minor UX tweak) | +| `minor` | A substantial new user-facing feature, such as a new slash command, a new built-in tool, or a new mode | | `major` | Breaking changes: incompatible config changes, renamed or removed commands/arguments, behavior semantics changes, and similar | +When in doubt between `patch` and `minor`: if the change improves an existing feature and the user-facing impact is small, choose `patch` even when the change is technically "new". Reserve `minor` for a substantial new capability that introduces something users could not do before. + ### Major Rule Never write `major` on your own. diff --git a/.changeset/expand-truncated-todo-list.md b/.changeset/expand-truncated-todo-list.md new file mode 100644 index 000000000..66f2fc7e6 --- /dev/null +++ b/.changeset/expand-truncated-todo-list.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Add a Ctrl+T shortcut to expand and collapse a truncated todo list. diff --git a/apps/kimi-code/src/tui/components/chrome/footer.ts b/apps/kimi-code/src/tui/components/chrome/footer.ts index 0d17dad63..daf08b3ce 100644 --- a/apps/kimi-code/src/tui/components/chrome/footer.ts +++ b/apps/kimi-code/src/tui/components/chrome/footer.ts @@ -58,6 +58,7 @@ const TOOLBAR_TIPS: readonly ToolbarTip[] = [ { text: 'ctrl+b: background task', priority: 2 }, { text: '/compact: compact context', priority: 2 }, { text: 'ctrl+o: expand tool output' }, + { text: 'ctrl+t: expand todo list' }, { text: '/tasks: background tasks' }, { text: 'shift+enter: newline' }, { text: '/init: generate AGENTS.md', priority: 2 }, diff --git a/apps/kimi-code/src/tui/components/chrome/todo-panel.ts b/apps/kimi-code/src/tui/components/chrome/todo-panel.ts index e52d77768..02bba03f2 100644 --- a/apps/kimi-code/src/tui/components/chrome/todo-panel.ts +++ b/apps/kimi-code/src/tui/components/chrome/todo-panel.ts @@ -99,6 +99,7 @@ export function selectVisibleTodos(todos: readonly TodoItem[]): VisibleTodos { export class TodoPanelComponent implements Component { private todos: readonly TodoItem[] = []; + private expanded = false; setTodos(todos: readonly TodoItem[]): void { this.todos = todos.map((t) => ({ title: t.title, status: t.status })); @@ -110,27 +111,53 @@ export class TodoPanelComponent implements Component { clear(): void { this.todos = []; + this.expanded = false; } isEmpty(): boolean { return this.todos.length === 0; } + /** True when the list exceeds the collapsed cap, i.e. there is something to expand. */ + hasOverflow(): boolean { + return this.todos.length > MAX_VISIBLE; + } + + setExpanded(expanded: boolean): void { + this.expanded = expanded; + } + + toggleExpanded(): void { + this.expanded = !this.expanded; + } + invalidate(): void {} render(width: number): string[] { if (this.todos.length === 0) return []; const c = currentTheme.palette; - const { rows, hidden } = selectVisibleTodos(this.todos); const lines: string[] = [ chalk.hex(c.border)('─'.repeat(width)), chalk.hex(c.primary).bold(' Todo'), ]; - for (const todo of rows) { - lines.push(renderRow(todo, c)); - } - if (hidden > 0) { - lines.push(chalk.hex(c.textDim)(` … +${hidden} more`)); + + if (this.expanded) { + for (const todo of this.todos) { + lines.push(renderRow(todo, c)); + } + if (this.todos.length > MAX_VISIBLE) { + lines.push( + chalk.hex(c.textDim)(` all ${String(this.todos.length)} items · ctrl+t to collapse`), + ); + } + } else { + const { rows, hidden } = selectVisibleTodos(this.todos); + for (const todo of rows) { + lines.push(renderRow(todo, c)); + } + if (hidden > 0) { + lines.push(chalk.hex(c.textDim)(` … +${hidden} more · ctrl+t to expand`)); + } } return lines.map((line) => truncateToWidth(line, width)); 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 1bbb743a0..1f931eb5a 100644 --- a/apps/kimi-code/src/tui/components/dialogs/help-panel.ts +++ b/apps/kimi-code/src/tui/components/dialogs/help-panel.ts @@ -34,6 +34,7 @@ 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-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' }, { keys: 'Ctrl-C', description: 'Interrupt stream / clear input' }, diff --git a/apps/kimi-code/src/tui/components/editor/custom-editor.ts b/apps/kimi-code/src/tui/components/editor/custom-editor.ts index 3ef7af6da..28fba5fea 100644 --- a/apps/kimi-code/src/tui/components/editor/custom-editor.ts +++ b/apps/kimi-code/src/tui/components/editor/custom-editor.ts @@ -121,6 +121,8 @@ export class CustomEditor extends Editor { public onCtrlS?: () => void; /** Return `true` to consume Ctrl+B; return `false`/`undefined` to fall through to the editor default (cursor-left). */ public onCtrlB?: () => boolean; + /** Return `true` to consume Ctrl+T (the todo list had overflow to toggle); return `false`/`undefined` to fall through to the editor default. */ + public onToggleTodoExpand?: () => boolean; public onUndo?: () => void; public onInsertNewline?: () => void; public onTextPaste?: () => void; @@ -358,6 +360,12 @@ export class CustomEditor extends Editor { if (this.onCtrlB?.() === true) return; } + if (matchesKey(normalized, Key.ctrl('t'))) { + // Only consume the key when the todo list actually has overflow to + // expand/collapse; otherwise fall through to the editor default. + if (this.onToggleTodoExpand?.() === true) return; + } + if (matchesKey(normalized, 'shift+tab')) { this.onShiftTab?.(); return; diff --git a/apps/kimi-code/src/tui/controllers/editor-keyboard.ts b/apps/kimi-code/src/tui/controllers/editor-keyboard.ts index d27e858e1..bc5b922b0 100644 --- a/apps/kimi-code/src/tui/controllers/editor-keyboard.ts +++ b/apps/kimi-code/src/tui/controllers/editor-keyboard.ts @@ -31,6 +31,7 @@ export interface EditorKeyboardHost { updateEditorBorderHighlight(text?: string): void; updateQueueDisplay(): void; toggleToolOutputExpansion(): void; + toggleTodoPanelExpansion(): void; detachCurrentForegroundTask(): void; hideSessionPicker(): void; stop(exitCode?: number): Promise; @@ -156,6 +157,16 @@ export class EditorKeyboardController { host.toggleToolOutputExpansion(); }; + editor.onToggleTodoExpand = (): boolean => { + if (!host.state.todoPanel.hasOverflow()) return false; + // Disarm a pending double-press exit confirmation so expanding the + // todo list in between two Ctrl-C presses does not accidentally exit. + this.clearPendingExit(); + host.track('shortcut_todo_expand'); + host.toggleTodoPanelExpansion(); + return true; + }; + editor.onCtrlS = () => { if (host.state.appState.streamingPhase === 'idle' || host.state.appState.isCompacting) return; const text = editor.getText().trim(); diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index c7f3951ec..67d8375c2 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -1765,6 +1765,11 @@ export class KimiTUI { this.state.ui.requestRender(); } + toggleTodoPanelExpansion(): void { + this.state.todoPanel.toggleExpanded(); + this.state.ui.requestRender(); + } + async detachCurrentForegroundTask(): Promise { const session = this.session; if (session === undefined) { diff --git a/apps/kimi-code/test/tui/components/editor/custom-editor.test.ts b/apps/kimi-code/test/tui/components/editor/custom-editor.test.ts index 5dce12c26..9e991ef3b 100644 --- a/apps/kimi-code/test/tui/components/editor/custom-editor.test.ts +++ b/apps/kimi-code/test/tui/components/editor/custom-editor.test.ts @@ -459,4 +459,14 @@ describe('CustomEditor shortcut telemetry hooks', () => { expect(onUndo).toHaveBeenCalledOnce(); }); + + it('invokes onToggleTodoExpand on Ctrl+T', () => { + const editor = makeEditor(); + const onToggleTodoExpand = vi.fn().mockReturnValue(true); + editor.onToggleTodoExpand = onToggleTodoExpand; + + editor.handleInput('\u0014'); + + expect(onToggleTodoExpand).toHaveBeenCalledOnce(); + }); }); diff --git a/apps/kimi-code/test/tui/components/panels/todo-panel.test.ts b/apps/kimi-code/test/tui/components/panels/todo-panel.test.ts index 04e3f1b88..a3644e8c2 100644 --- a/apps/kimi-code/test/tui/components/panels/todo-panel.test.ts +++ b/apps/kimi-code/test/tui/components/panels/todo-panel.test.ts @@ -88,6 +88,73 @@ describe('TodoPanelComponent', () => { const out = strip(panel.render(80).join('\n')); expect(out).toMatch(/\+2 more/); }); + + const many = (n: number): TodoItem[] => + Array.from({ length: n }, (_, i) => ({ title: `t${i}`, status: 'pending' as const })); + + it('hasOverflow() is false when count <= 5 and true when count > 5', () => { + const panel = new TodoPanelComponent(); + panel.setTodos(many(5)); + expect(panel.hasOverflow()).toBe(false); + panel.setTodos(many(6)); + expect(panel.hasOverflow()).toBe(true); + }); + + it('collapsed footer advertises "ctrl+t to expand"', () => { + const panel = new TodoPanelComponent(); + panel.setTodos(many(7)); + const out = strip(panel.render(80).join('\n')); + expect(out).toMatch(/\+2 more/); + expect(out).toMatch(/ctrl\+t to expand/); + }); + + it('renders every todo with a collapse hint when expanded', () => { + const panel = new TodoPanelComponent(); + panel.setTodos(many(7)); + panel.setExpanded(true); + const out = strip(panel.render(80).join('\n')); + expect(out).toMatch(/t0/); + expect(out).toMatch(/t6/); + expect(out).not.toMatch(/\+\d+ more/); + expect(out).toMatch(/ctrl\+t to collapse/); + }); + + it('toggleExpanded() flips between collapsed and expanded', () => { + const panel = new TodoPanelComponent(); + panel.setTodos(many(7)); + expect(strip(panel.render(80).join('\n'))).toMatch(/\+2 more/); + panel.toggleExpanded(); + expect(strip(panel.render(80).join('\n'))).toMatch(/ctrl\+t to collapse/); + panel.toggleExpanded(); + expect(strip(panel.render(80).join('\n'))).toMatch(/\+2 more/); + }); + + it('setTodos() keeps the expanded state across list updates', () => { + const panel = new TodoPanelComponent(); + panel.setTodos(many(7)); + panel.setExpanded(true); + panel.setTodos([ + { title: 'u0', status: 'pending' }, + { title: 'u1', status: 'pending' }, + { title: 'u2', status: 'pending' }, + { title: 'u3', status: 'pending' }, + { title: 'u4', status: 'pending' }, + { title: 'u5', status: 'pending' }, + { title: 'u6', status: 'pending' }, + ]); + const out = strip(panel.render(80).join('\n')); + expect(out).toMatch(/u6/); + expect(out).toMatch(/ctrl\+t to collapse/); + }); + + it('clear() resets the expanded state', () => { + const panel = new TodoPanelComponent(); + panel.setTodos(many(7)); + panel.setExpanded(true); + panel.clear(); + panel.setTodos(many(7)); + expect(strip(panel.render(80).join('\n'))).toMatch(/\+2 more/); + }); }); describe('selectVisibleTodos', () => { diff --git a/docs/en/reference/keyboard.md b/docs/en/reference/keyboard.md index 5c4eea7ff..1ef344f3e 100644 --- a/docs/en/reference/keyboard.md +++ b/docs/en/reference/keyboard.md @@ -14,6 +14,7 @@ The following keys are always available in the input box: | `Esc` | Close a popup / cancel completion / interrupt streaming output or context compaction | | `Ctrl-C` | Interrupt the current streaming output, or clear the input box | | `Ctrl-D` | Exit Kimi Code CLI when the input box is empty | +| `Ctrl-T` | Expand or collapse the todo list when it is truncated | Pressing `Ctrl-C` **during streaming** cancels immediately — no second confirmation needed. diff --git a/docs/zh/reference/keyboard.md b/docs/zh/reference/keyboard.md index ddc6db4b9..36fe5a51c 100644 --- a/docs/zh/reference/keyboard.md +++ b/docs/zh/reference/keyboard.md @@ -14,6 +14,7 @@ Kimi Code CLI 的 TUI 交互模式支持一套键盘快捷键。键位按使用 | `Esc` | 关闭弹窗 / 取消补全 / 中断流式输出或上下文压缩 | | `Ctrl-C` | 中断当前流式输出,或清空输入框 | | `Ctrl-D` | 在输入框为空时退出 Kimi Code CLI | +| `Ctrl-T` | 待办列表被截断时,展开或折叠完整列表 | **流式输出期间**按 `Ctrl-C` 会立即取消,无需二次确认。