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`] = `""`;