Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .agents/skills/gen-changesets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions .changeset/expand-truncated-todo-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Add a Ctrl+T shortcut to expand and collapse a truncated todo list.
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/components/chrome/footer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
39 changes: 33 additions & 6 deletions apps/kimi-code/src/tui/components/chrome/todo-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
Expand All @@ -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));
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/components/dialogs/help-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
8 changes: 8 additions & 0 deletions apps/kimi-code/src/tui/components/editor/custom-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions apps/kimi-code/src/tui/controllers/editor-keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
Expand Down Expand Up @@ -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;
Comment thread
liruifengv marked this conversation as resolved.
};

editor.onCtrlS = () => {
if (host.state.appState.streamingPhase === 'idle' || host.state.appState.isCompacting) return;
const text = editor.getText().trim();
Expand Down
5 changes: 5 additions & 0 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1765,6 +1765,11 @@ export class KimiTUI {
this.state.ui.requestRender();
}

toggleTodoPanelExpansion(): void {
this.state.todoPanel.toggleExpanded();
this.state.ui.requestRender();
}

async detachCurrentForegroundTask(): Promise<void> {
const session = this.session;
if (session === undefined) {
Expand Down
10 changes: 10 additions & 0 deletions apps/kimi-code/test/tui/components/editor/custom-editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
67 changes: 67 additions & 0 deletions apps/kimi-code/test/tui/components/panels/todo-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
1 change: 1 addition & 0 deletions docs/en/reference/keyboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions docs/zh/reference/keyboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Kimi Code CLI 的 TUI 交互模式支持一套键盘快捷键。键位按使用
| `Esc` | 关闭弹窗 / 取消补全 / 中断流式输出或上下文压缩 |
| `Ctrl-C` | 中断当前流式输出,或清空输入框 |
| `Ctrl-D` | 在输入框为空时退出 Kimi Code CLI |
| `Ctrl-T` | 待办列表被截断时,展开或折叠完整列表 |

**流式输出期间**按 `Ctrl-C` 会立即取消,无需二次确认。

Expand Down
Loading