diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx
index cf16b51cbc..327d8ee3e8 100644
--- a/packages/cli/src/ui/components/Composer.tsx
+++ b/packages/cli/src/ui/components/Composer.tsx
@@ -66,6 +66,8 @@ export const Composer = () => {
+
+
{
)}
-
-
{uiState.isInputActive && (
}) as unknown as HistoryItem;
describe('', () => {
- const mockHistoryItem = createTodoHistoryItem([
- { description: 'Pending Task', status: 'pending' },
- { description: 'In Progress Task', status: 'in_progress' },
- { description: 'Completed Task', status: 'completed' },
- ]);
-
const renderWithUiState = (uiState: Partial) =>
render(
@@ -44,99 +38,106 @@ describe('', () => {
,
);
- it('renders null when no todos are in the history', () => {
- const { lastFrame } = renderWithUiState({ history: [] });
- expect(lastFrame()).toMatchSnapshot();
- });
+ it.each([true, false])(
+ 'renders null when no todos are in the history',
+ (showFullTodos) => {
+ const { lastFrame } = renderWithUiState({ history: [], showFullTodos });
+ expect(lastFrame()).toMatchSnapshot();
+ },
+ );
- it('renders null when todos exist but none are in progress and full view is off', () => {
- const historyWithNoInProgress = createTodoHistoryItem([
- { description: 'Pending Task', status: 'pending' },
- { description: 'In Progress Task', status: 'cancelled' },
- { description: 'Completed Task', status: 'completed' },
- ]);
+ it.each([true, false])(
+ 'renders null when todo list is empty',
+ (showFullTodos) => {
+ const { lastFrame } = renderWithUiState({
+ history: [createTodoHistoryItem([])],
+ showFullTodos,
+ });
+ expect(lastFrame()).toMatchSnapshot();
+ },
+ );
+
+ it.each([true, false])(
+ 'renders when todos exist but none are in progress',
+ (showFullTodos) => {
+ const { lastFrame } = renderWithUiState({
+ history: [
+ createTodoHistoryItem([
+ { description: 'Pending Task', status: 'pending' },
+ { description: 'In Progress Task', status: 'cancelled' },
+ { description: 'Completed Task', status: 'completed' },
+ ]),
+ ],
+ showFullTodos,
+ });
+ expect(lastFrame()).toMatchSnapshot();
+ },
+ );
+
+ it.each([true, false])(
+ 'renders when todos exist and one is in progress',
+ (showFullTodos) => {
+ const { lastFrame } = renderWithUiState({
+ history: [
+ createTodoHistoryItem([
+ { description: 'Pending Task', status: 'pending' },
+ { description: 'Task 2', status: 'in_progress' },
+ { description: 'In Progress Task', status: 'cancelled' },
+ { description: 'Completed Task', status: 'completed' },
+ ]),
+ ],
+ showFullTodos,
+ });
+ expect(lastFrame()).toMatchSnapshot();
+ },
+ );
+
+ it.each([true, false])(
+ 'renders a todo list with long descriptions that wrap when full view is on',
+ (showFullTodos) => {
+ const { lastFrame } = render(
+
+
+
+
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ },
+ );
+
+ it('renders the most recent todo list when multiple write_todos calls are in history', () => {
const { lastFrame } = renderWithUiState({
- history: [historyWithNoInProgress],
- showFullTodos: false,
- });
- expect(lastFrame()).toMatchSnapshot();
- });
-
- it('renders an empty todo list when full view is on', () => {
- const emptyTodosHistoryItem = createTodoHistoryItem([]);
- const { lastFrame } = renderWithUiState({
- history: [emptyTodosHistoryItem],
- showFullTodos: true,
- });
- expect(lastFrame()).toMatchSnapshot();
- });
-
- it('renders a todo list with various statuses when full view is on', () => {
- const variousTodosHistoryItem = createTodoHistoryItem([
- { description: 'Task 1', status: 'pending' },
- { description: 'Task 2', status: 'in_progress' },
- { description: 'Task 3', status: 'completed' },
- { description: 'Task 4', status: 'cancelled' },
- ]);
- const { lastFrame } = renderWithUiState({
- history: [variousTodosHistoryItem],
- showFullTodos: true,
- });
- expect(lastFrame()).toMatchSnapshot();
- });
-
- it('renders a todo list with long descriptions that wrap when full view is on', () => {
- const longDescriptionTodosHistoryItem = createTodoHistoryItem([
- {
- description:
- 'This is a very long description for a pending task that should wrap around multiple lines when the terminal width is constrained.',
- status: 'pending',
- },
- {
- description:
- 'Another completed task with an equally verbose description to test wrapping behavior.',
- status: 'completed',
- },
- ]);
- const { lastFrame } = render(
-
-
-
-
- ,
- );
- expect(lastFrame()).toMatchSnapshot();
- });
-
- it('renders a single todo item when full view is on', () => {
- const singleTodoHistoryItem = createTodoHistoryItem([
- { description: 'Single task', status: 'pending' },
- ]);
- const { lastFrame } = renderWithUiState({
- history: [singleTodoHistoryItem],
- showFullTodos: true,
- });
- expect(lastFrame()).toMatchSnapshot();
- });
-
- it('renders only the in-progress task when full view is off', () => {
- const { lastFrame } = renderWithUiState({
- history: [mockHistoryItem],
- showFullTodos: false,
- });
- expect(lastFrame()).toMatchSnapshot();
- });
-
- it('renders the full todo list when full view is on', () => {
- const { lastFrame } = renderWithUiState({
- history: [mockHistoryItem],
+ history: [
+ createTodoHistoryItem([
+ { description: 'Older Task 1', status: 'completed' },
+ { description: 'Older Task 2', status: 'pending' },
+ ]),
+ createTodoHistoryItem([
+ { description: 'Newer Task 1', status: 'pending' },
+ { description: 'Newer Task 2', status: 'in_progress' },
+ ]),
+ ],
showFullTodos: true,
});
expect(lastFrame()).toMatchSnapshot();
diff --git a/packages/cli/src/ui/components/messages/Todo.tsx b/packages/cli/src/ui/components/messages/Todo.tsx
index ccea8e1f09..9d27c24852 100644
--- a/packages/cli/src/ui/components/messages/Todo.tsx
+++ b/packages/cli/src/ui/components/messages/Todo.tsx
@@ -16,6 +16,31 @@ import { useUIState } from '../../contexts/UIStateContext.js';
import { useMemo } from 'react';
import type { HistoryItemToolGroup } from '../../types.js';
+const TodoTitleDisplay: React.FC<{ todos: TodoList }> = ({ todos }) => {
+ const score = useMemo(() => {
+ let total = 0;
+ let completed = 0;
+ for (const todo of todos.todos) {
+ if (todo.status !== 'cancelled') {
+ total += 1;
+ if (todo.status === 'completed') {
+ completed += 1;
+ }
+ }
+ }
+ return `${completed}/${total}`;
+ }, [todos]);
+
+ return (
+
+
+ π Todo
+
+ {score} (ctrl+t to toggle)
+
+ );
+};
+
const TodoStatusDisplay: React.FC<{ status: TodoStatus }> = ({ status }) => {
switch (status) {
case 'completed':
@@ -30,13 +55,16 @@ const TodoStatusDisplay: React.FC<{ status: TodoStatus }> = ({ status }) => {
}
};
-const TodoItemDisplay: React.FC<{ todo: Todo }> = ({ todo }) => (
-
-
-
-
+const TodoItemDisplay: React.FC<{ todo: Todo; wrap?: 'truncate' }> = ({
+ todo,
+ wrap,
+}) => (
+
+
- {todo.description}
+
+ {todo.description}
+
);
@@ -72,50 +100,37 @@ export const TodoTray: React.FC = () => {
return todos.todos.find((todo) => todo.status === 'in_progress') || null;
}, [todos]);
- if (todos === null) {
- return null;
- }
-
- if (uiState.showFullTodos) {
- return (
-
-
- π Todo:
- (ctrl+t to collapse)
-
-
-
-
-
-
- );
- }
-
- if (inProgress === null) {
+ if (todos === null || !todos.todos || todos.todos.length === 0) {
return null;
}
return (
-
- π Todo:
- (ctrl+t to expand)
-
-
+ {uiState.showFullTodos ? (
+
+
+
+
+ ) : (
+
+
+
+
+ {inProgress && (
+
+
+
+ )}
+
+ )}
);
};
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/Todo.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/Todo.test.tsx.snap
index 12c2fd533a..a7001a4eda 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/Todo.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/Todo.test.tsx.snap
@@ -1,63 +1,62 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[` > renders a single todo item when full view is on 1`] = `
-"ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-β π Todo:(ctrl+t to collapse) β
-β β
-β β Single task β"
-`;
-
exports[` > renders a todo list with long descriptions that wrap when full view is on 1`] = `
-"ββββββββββββββββββββββββββββββ
-β π Todo:(ctrl+t to β
-β collapse) β
-β β
-β β This is a very long β
-β description for a β
-β pending task that β
-β should wrap around β
-β multiple lines when β
-β the terminal width β
-β is constrained. β
-β β
-β β Another completed β
-β task with an β
-β equally verbose β
-β description to β
-β test wrapping β
-β behavior. β"
+"ββββββββββββββββββββββββββββββββββββββββββββββββββ
+ π Todo 1/2 (ctrl+t to toggle)
+
+ Β» This is a very long description for a pending
+ task that should wrap around multiple lines
+ when the terminal width is constrained.
+ β Another completed task with an equally verbose
+ description to test wrapping behavior."
`;
-exports[` > renders a todo list with various statuses when full view is on 1`] = `
-"ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-β π Todo:(ctrl+t to collapse) β
-β β
-β β Task 1 β
-β Β» Task 2 β
-β β Task 3 β
-β β Task 4 β"
-`;
-
-exports[` > renders an empty todo list when full view is on 1`] = `
-"ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-β π Todo:(ctrl+t to collapse) β
-β β"
+exports[` > renders a todo list with long descriptions that wrap when full view is on 2`] = `
+"ββββββββββββββββββββββββββββββββββββββββββββββββββ
+ π Todo 1/2 (ctrl+t to toggle) Β» This is a verβ¦"
`;
exports[` > renders null when no todos are in the history 1`] = `""`;
-exports[` > renders null when todos exist but none are in progress and full view is off 1`] = `""`;
+exports[` > renders null when no todos are in the history 2`] = `""`;
-exports[` > renders only the in-progress task when full view is off 1`] = `
-"ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-β π Todo:(ctrl+t to expand)Β» In Progress Task β"
+exports[` > renders null when todo list is empty 1`] = `""`;
+
+exports[` > renders null when todo list is empty 2`] = `""`;
+
+exports[` > renders the most recent todo list when multiple write_todos calls are in history 1`] = `
+"ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ π Todo 0/2 (ctrl+t to toggle)
+
+ β Newer Task 1
+ Β» Newer Task 2"
`;
-exports[` > renders the full todo list when full view is on 1`] = `
-"ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-β π Todo:(ctrl+t to collapse) β
-β β
-β β Pending Task β
-β Β» In Progress Task β
-β β Completed Task β"
+exports[` > renders when todos exist and one is in progress 1`] = `
+"ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ π Todo 1/3 (ctrl+t to toggle)
+
+ β Pending Task
+ Β» Task 2
+ β In Progress Task
+ β Completed Task"
+`;
+
+exports[` > renders when todos exist and one is in progress 2`] = `
+"ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ π Todo 1/3 (ctrl+t to toggle) Β» Task 2"
+`;
+
+exports[` > renders when todos exist but none are in progress 1`] = `
+"ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ π Todo 1/2 (ctrl+t to toggle)
+
+ β Pending Task
+ β In Progress Task
+ β Completed Task"
+`;
+
+exports[` > renders when todos exist but none are in progress 2`] = `
+"ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ π Todo 1/2 (ctrl+t to toggle)"
`;
diff --git a/packages/core/src/tools/write-todos.ts b/packages/core/src/tools/write-todos.ts
index 74a1efbd88..896861613d 100644
--- a/packages/core/src/tools/write-todos.ts
+++ b/packages/core/src/tools/write-todos.ts
@@ -14,6 +14,13 @@ import {
} from './tools.js';
import { WRITE_TODOS_TOOL_NAME } from './tool-names.js';
+const TODO_STATUSES = [
+ 'pending',
+ 'in_progress',
+ 'completed',
+ 'cancelled',
+] as const;
+
// Inspired by langchain/deepagents.
export const WRITE_TODOS_DESCRIPTION = `This tool can help you list out the current subtasks that are required to be completed for a given user request. The list of subtasks helps you keep track of the current task, organize complex queries and help ensure that you don't miss any steps. With this list, the user can also see the current progress you are making in executing a given task.
@@ -152,7 +159,7 @@ export class WriteTodosTool extends BaseDeclarativeTool<
status: {
type: 'string',
description: 'The current status of the task.',
- enum: ['pending', 'in_progress', 'completed'],
+ enum: TODO_STATUSES,
},
},
required: ['description', 'status'],
@@ -179,8 +186,8 @@ export class WriteTodosTool extends BaseDeclarativeTool<
if (typeof todo.description !== 'string' || !todo.description.trim()) {
return 'Each todo must have a non-empty description string';
}
- if (!['pending', 'in_progress', 'completed'].includes(todo.status)) {
- return 'Each todo must have a valid status (pending, in_progress, or completed)';
+ if (!TODO_STATUSES.includes(todo.status)) {
+ return `Each todo must have a valid status (${TODO_STATUSES.join(', ')})`;
}
}