mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat(tui): show hidden todo status breakdown in collapsed panel (#1122)
This commit is contained in:
parent
36cbdb29c0
commit
820d77ab4c
3 changed files with 113 additions and 3 deletions
5
.changeset/collapsed-todo-status-breakdown.md
Normal file
5
.changeset/collapsed-todo-status-breakdown.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Show the done / in progress / pending breakdown of hidden todos in the collapsed todo panel.
|
||||
|
|
@ -28,6 +28,7 @@ const MAX_VISIBLE = 5;
|
|||
export interface VisibleTodos {
|
||||
readonly rows: readonly TodoItem[];
|
||||
readonly hidden: number;
|
||||
readonly hiddenCounts: Record<TodoStatus, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -49,7 +50,11 @@ export interface VisibleTodos {
|
|||
*/
|
||||
export function selectVisibleTodos(todos: readonly TodoItem[]): VisibleTodos {
|
||||
if (todos.length <= MAX_VISIBLE) {
|
||||
return { rows: [...todos], hidden: 0 };
|
||||
return {
|
||||
rows: [...todos],
|
||||
hidden: 0,
|
||||
hiddenCounts: { done: 0, in_progress: 0, pending: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const inProgress: number[] = [];
|
||||
|
|
@ -91,9 +96,18 @@ export function selectVisibleTodos(todos: readonly TodoItem[]): VisibleTodos {
|
|||
}
|
||||
|
||||
const sortedIdx = [...picked].toSorted((a, b) => a - b);
|
||||
|
||||
const hiddenCounts: Record<TodoStatus, number> = { done: 0, in_progress: 0, pending: 0 };
|
||||
for (const [i, todo] of todos.entries()) {
|
||||
if (!picked.has(i)) {
|
||||
hiddenCounts[todo.status] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rows: sortedIdx.map((i) => todos[i] as TodoItem),
|
||||
hidden: todos.length - sortedIdx.length,
|
||||
hiddenCounts,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -151,12 +165,16 @@ export class TodoPanelComponent implements Component {
|
|||
);
|
||||
}
|
||||
} else {
|
||||
const { rows, hidden } = selectVisibleTodos(this.todos);
|
||||
const { rows, hidden, hiddenCounts } = 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`));
|
||||
const distribution = formatHiddenCounts(hiddenCounts);
|
||||
const suffix = distribution.length > 0 ? ` (${distribution})` : '';
|
||||
lines.push(
|
||||
chalk.hex(c.textDim)(` … +${hidden} more${suffix} · ctrl+t to expand`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -191,3 +209,15 @@ function styleTitle(title: string, status: TodoStatus, colors: ColorPalette): st
|
|||
return chalk.hex(colors.text)(title);
|
||||
}
|
||||
}
|
||||
|
||||
const STATUS_LABELS: readonly { status: TodoStatus; label: string }[] = [
|
||||
{ status: 'done', label: 'done' },
|
||||
{ status: 'in_progress', label: 'in progress' },
|
||||
{ status: 'pending', label: 'pending' },
|
||||
];
|
||||
|
||||
export function formatHiddenCounts(counts: Record<TodoStatus, number>): string {
|
||||
return STATUS_LABELS.filter(({ status }) => counts[status] > 0)
|
||||
.map(({ status, label }) => `${counts[status]} ${label}`)
|
||||
.join(' · ');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|||
|
||||
import {
|
||||
TodoPanelComponent,
|
||||
formatHiddenCounts,
|
||||
selectVisibleTodos,
|
||||
type TodoItem,
|
||||
} from '#/tui/components/chrome/todo-panel';
|
||||
|
|
@ -108,6 +109,43 @@ describe('TodoPanelComponent', () => {
|
|||
expect(out).toMatch(/ctrl\+t to expand/);
|
||||
});
|
||||
|
||||
it('collapsed footer shows hidden status distribution', () => {
|
||||
const panel = new TodoPanelComponent();
|
||||
panel.setTodos([
|
||||
...Array.from({ length: 6 }, (_, i) => ({
|
||||
title: `ip${i}`,
|
||||
status: 'in_progress' as const,
|
||||
})),
|
||||
...Array.from({ length: 3 }, (_, i) => ({ title: `d${i}`, status: 'done' as const })),
|
||||
...Array.from({ length: 3 }, (_, i) => ({ title: `p${i}`, status: 'pending' as const })),
|
||||
]);
|
||||
const out = strip(panel.render(80).join('\n'));
|
||||
expect(out).toMatch(/\+7 more \(3 done · 1 in progress · 3 pending\)/);
|
||||
expect(out).toMatch(/ctrl\+t to expand/);
|
||||
});
|
||||
|
||||
it('collapsed footer omits zero-count statuses', () => {
|
||||
const panel = new TodoPanelComponent();
|
||||
panel.setTodos(
|
||||
Array.from({ length: 8 }, (_, i) => ({ title: `d${i}`, status: 'done' as const })),
|
||||
);
|
||||
const out = strip(panel.render(80).join('\n'));
|
||||
expect(out).toMatch(/\+3 more \(3 done\)/);
|
||||
expect(out).not.toMatch(/0 in progress/);
|
||||
expect(out).not.toMatch(/0 pending/);
|
||||
});
|
||||
|
||||
it('expanded footer does not include status distribution', () => {
|
||||
const panel = new TodoPanelComponent();
|
||||
panel.setTodos(
|
||||
Array.from({ length: 8 }, (_, i) => ({ title: `d${i}`, status: 'done' as const })),
|
||||
);
|
||||
panel.setExpanded(true);
|
||||
const out = strip(panel.render(80).join('\n'));
|
||||
expect(out).toMatch(/all 8 items · ctrl\+t to collapse/);
|
||||
expect(out).not.toMatch(/\d+ done ·/);
|
||||
});
|
||||
|
||||
it('renders every todo with a collapse hint when expanded', () => {
|
||||
const panel = new TodoPanelComponent();
|
||||
panel.setTodos(many(7));
|
||||
|
|
@ -305,4 +343,41 @@ describe('selectVisibleTodos', () => {
|
|||
expect(rows.map((r) => r.title)).toEqual(['ip0', 'ip1', 'ip2', 'ip3', 'ip4']);
|
||||
expect(hidden).toBe(2);
|
||||
});
|
||||
|
||||
it('returns hiddenCounts reflecting the hidden items', () => {
|
||||
const todos: TodoItem[] = [
|
||||
...Array.from({ length: 6 }, (_, i) => T(`ip${i}`, 'in_progress')),
|
||||
...Array.from({ length: 3 }, (_, i) => T(`d${i}`, 'done')),
|
||||
...Array.from({ length: 3 }, (_, i) => T(`p${i}`, 'pending')),
|
||||
];
|
||||
const { hidden, hiddenCounts } = selectVisibleTodos(todos);
|
||||
expect(hidden).toBe(7);
|
||||
expect(hiddenCounts).toEqual({ done: 3, in_progress: 1, pending: 3 });
|
||||
});
|
||||
|
||||
it('returns zero hiddenCounts when count <= 5', () => {
|
||||
const todos: TodoItem[] = [T('a', 'done'), T('b', 'in_progress'), T('c', 'pending')];
|
||||
const { hidden, hiddenCounts } = selectVisibleTodos(todos);
|
||||
expect(hidden).toBe(0);
|
||||
expect(hiddenCounts).toEqual({ done: 0, in_progress: 0, pending: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatHiddenCounts', () => {
|
||||
it('formats all three statuses in done / in progress / pending order', () => {
|
||||
expect(formatHiddenCounts({ done: 2, in_progress: 1, pending: 3 })).toBe(
|
||||
'2 done · 1 in progress · 3 pending',
|
||||
);
|
||||
});
|
||||
|
||||
it('omits zero-count statuses', () => {
|
||||
expect(formatHiddenCounts({ done: 5, in_progress: 0, pending: 0 })).toBe('5 done');
|
||||
expect(formatHiddenCounts({ done: 0, in_progress: 2, pending: 3 })).toBe(
|
||||
'2 in progress · 3 pending',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns empty string when all counts are zero', () => {
|
||||
expect(formatHiddenCounts({ done: 0, in_progress: 0, pending: 0 })).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue