diff --git a/packages/cli/src/ui/components/messages/Todo.test.tsx b/packages/cli/src/ui/components/messages/Todo.test.tsx
index 0ca9e0a06d..e149a2665f 100644
--- a/packages/cli/src/ui/components/messages/Todo.test.tsx
+++ b/packages/cli/src/ui/components/messages/Todo.test.tsx
@@ -7,67 +7,15 @@
import { render } from 'ink-testing-library';
import { describe, it, expect } from 'vitest';
import { Box } from 'ink';
-import { TodoTray, TodoListDisplay } from './Todo.js';
-import type { TodoList, TodoStatus } from '@google/gemini-cli-core';
+import { TodoTray } from './Todo.js';
+import type { Todo } from '@google/gemini-cli-core';
import type { UIState } from '../../contexts/UIStateContext.js';
import { UIStateContext } from '../../contexts/UIStateContext.js';
import type { HistoryItem } from '../../types.js';
import { ToolCallStatus } from '../../types.js';
-describe('', () => {
- it('renders an empty todo list correctly', () => {
- const todos: TodoList = { todos: [] };
- const { lastFrame } = render();
- expect(lastFrame()).toMatchSnapshot();
- });
-
- it('renders a todo list with various statuses correctly', () => {
- const todos: TodoList = {
- todos: [
- { description: 'Task 1', status: 'pending' as TodoStatus },
- { description: 'Task 2', status: 'in_progress' as TodoStatus },
- { description: 'Task 3', status: 'completed' as TodoStatus },
- { description: 'Task 4', status: 'cancelled' as TodoStatus },
- ],
- };
- const { lastFrame } = render();
- expect(lastFrame()).toMatchSnapshot();
- });
-
- it('renders a todo list with long descriptions that wrap', () => {
- const todos: TodoList = {
- todos: [
- {
- 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' as TodoStatus,
- },
- {
- description:
- 'Another completed task with an equally verbose description to test wrapping behavior.',
- status: 'completed' as TodoStatus,
- },
- ],
- };
- const { lastFrame } = render(
-
-
- ,
- );
- expect(lastFrame()).toMatchSnapshot();
- });
-
- it('renders a single todo item', () => {
- const todos: TodoList = {
- todos: [{ description: 'Single task', status: 'pending' as TodoStatus }],
- };
- const { lastFrame } = render();
- expect(lastFrame()).toMatchSnapshot();
- });
-});
-
-describe('', () => {
- const mockHistoryItem = {
+const createTodoHistoryItem = (todos: Todo[]): HistoryItem =>
+ ({
type: 'tool_group',
id: '1',
tools: [
@@ -76,15 +24,18 @@ describe('', () => {
callId: 'tool-1',
status: ToolCallStatus.Success,
resultDisplay: {
- todos: [
- { description: 'Pending Task', status: 'pending' },
- { description: 'In Progress Task', status: 'in_progress' },
- { description: 'Completed Task', status: 'completed' },
- ],
+ todos,
},
},
],
- } as unknown as HistoryItem;
+ }) 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(
@@ -99,24 +50,11 @@ describe('', () => {
});
it('renders null when todos exist but none are in progress and full view is off', () => {
- const historyWithNoInProgress = {
- type: 'tool_group',
- id: '1',
- tools: [
- {
- name: 'write_todos_list',
- callId: 'tool-1',
- status: ToolCallStatus.Success,
- resultDisplay: {
- todos: [
- { description: 'Pending Task', status: 'pending' },
- { description: 'In Progress Task', status: 'cancelled' },
- { description: 'Completed Task', status: 'completed' },
- ],
- },
- },
- ],
- } as unknown as HistoryItem;
+ const historyWithNoInProgress = createTodoHistoryItem([
+ { description: 'Pending Task', status: 'pending' },
+ { description: 'In Progress Task', status: 'cancelled' },
+ { description: 'Completed Task', status: 'completed' },
+ ]);
const { lastFrame } = renderWithUiState({
history: [historyWithNoInProgress],
showFullTodos: false,
@@ -124,6 +62,70 @@ describe('', () => {
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],
diff --git a/packages/cli/src/ui/components/messages/Todo.tsx b/packages/cli/src/ui/components/messages/Todo.tsx
index da6932d13b..ccea8e1f09 100644
--- a/packages/cli/src/ui/components/messages/Todo.tsx
+++ b/packages/cli/src/ui/components/messages/Todo.tsx
@@ -16,17 +16,6 @@ import { useUIState } from '../../contexts/UIStateContext.js';
import { useMemo } from 'react';
import type { HistoryItemToolGroup } from '../../types.js';
-const TodoItemDisplay: React.FC<{ todo: Todo }> = ({ todo }) => (
-
-
-
-
-
- {todo.description}
-
-
-);
-
const TodoStatusDisplay: React.FC<{ status: TodoStatus }> = ({ status }) => {
switch (status) {
case 'completed':
@@ -41,6 +30,17 @@ const TodoStatusDisplay: React.FC<{ status: TodoStatus }> = ({ status }) => {
}
};
+const TodoItemDisplay: React.FC<{ todo: Todo }> = ({ todo }) => (
+
+
+
+
+
+ {todo.description}
+
+
+);
+
export const TodoTray: React.FC = () => {
const uiState = useUIState();
@@ -120,11 +120,11 @@ export const TodoTray: React.FC = () => {
);
};
-export interface TodoListDisplayProps {
+interface TodoListDisplayProps {
todos: TodoList;
}
-export const TodoListDisplay: React.FC = ({ todos }) => (
+const TodoListDisplay: React.FC = ({ todos }) => (
{todos.todos.map((todo: Todo, index: number) => (
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx
index c4326a1700..e031afa431 100644
--- a/packages/cli/src/ui/components/messages/ToolMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx
@@ -13,7 +13,6 @@ import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import { AnsiOutputText } from '../AnsiOutput.js';
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
-import { TodoListDisplay } from './Todo.js';
import { ShellInputPrompt } from '../ShellInputPrompt.js';
import {
SHELL_COMMAND_NAME,
@@ -21,7 +20,7 @@ import {
TOOL_STATUS,
} from '../../constants.js';
import { theme } from '../../semantic-colors.js';
-import type { AnsiOutput, Config, TodoList } from '@google/gemini-cli-core';
+import type { AnsiOutput, Config } from '@google/gemini-cli-core';
import { useUIState } from '../../contexts/UIStateContext.js';
const STATIC_HEIGHT = 1;
@@ -173,7 +172,8 @@ export const ToolMessage: React.FC = ({
/>
) : typeof resultDisplay === 'object' &&
'todos' in resultDisplay ? (
-
+ // display nothing, as the TodoTray will handle rendering todos
+ <>>
) : (
> renders a single todo item 1`] = `"☐ Single task"`;
-
-exports[` > renders a todo list with long descriptions that wrap 1`] = `
-"☐ 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 single todo item when full view is on 1`] = `
+"┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
+│ 📝 Todo:(ctrl+t to collapse) │
+│ │
+│ ☐ Single task │"
`;
-exports[` > renders a todo list with various statuses correctly 1`] = `
-"☐ Task 1
-» Task 2
-✓ Task 3
-✗ Task 4"
+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. │"
`;
-exports[` > renders an empty todo list correctly 1`] = `""`;
+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 null when no todos are in the history 1`] = `""`;
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/TodoListDisplay.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/TodoListDisplay.test.tsx.snap
deleted file mode 100644
index 2f63ae86e1..0000000000
--- a/packages/cli/src/ui/components/messages/__snapshots__/TodoListDisplay.test.tsx.snap
+++ /dev/null
@@ -1,22 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[` > renders a single todo item 1`] = `"☐ Single task"`;
-
-exports[` > renders a todo list with long descriptions that wrap 1`] = `
-"☐ 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 correctly 1`] = `
-"☐ Task 1
-» Task 2
-✓ Task 3
-✗ Task 4"
-`;
-
-exports[` > renders an empty todo list correctly 1`] = `""`;