Don't display todo in history (#11516)

This commit is contained in:
Tommaso Sciortino
2025-10-20 10:36:55 -07:00
committed by GitHub
parent 71ecc401c3
commit 35afab31e1
5 changed files with 137 additions and 137 deletions

View File

@@ -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('<TodoListDisplay />', () => {
it('renders an empty todo list correctly', () => {
const todos: TodoList = { todos: [] };
const { lastFrame } = render(<TodoListDisplay todos={todos} />);
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(<TodoListDisplay todos={todos} />);
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(
<Box width="30">
<TodoListDisplay todos={todos} />
</Box>,
);
expect(lastFrame()).toMatchSnapshot();
});
it('renders a single todo item', () => {
const todos: TodoList = {
todos: [{ description: 'Single task', status: 'pending' as TodoStatus }],
};
const { lastFrame } = render(<TodoListDisplay todos={todos} />);
expect(lastFrame()).toMatchSnapshot();
});
});
describe('<TodoTray />', () => {
const mockHistoryItem = {
const createTodoHistoryItem = (todos: Todo[]): HistoryItem =>
({
type: 'tool_group',
id: '1',
tools: [
@@ -76,15 +24,18 @@ describe('<TodoTray />', () => {
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('<TodoTray />', () => {
const mockHistoryItem = createTodoHistoryItem([
{ description: 'Pending Task', status: 'pending' },
{ description: 'In Progress Task', status: 'in_progress' },
{ description: 'Completed Task', status: 'completed' },
]);
const renderWithUiState = (uiState: Partial<UIState>) =>
render(
@@ -99,24 +50,11 @@ describe('<TodoTray />', () => {
});
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('<TodoTray />', () => {
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(
<Box width="30">
<UIStateContext.Provider
value={
{
history: [longDescriptionTodosHistoryItem],
showFullTodos: true,
} as UIState
}
>
<TodoTray />
</UIStateContext.Provider>
</Box>,
);
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],

View File

@@ -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 }) => (
<Box flexDirection="row">
<Box marginRight={1}>
<TodoStatusDisplay status={todo.status} />
</Box>
<Box flexShrink={1}>
<Text color={theme.text.primary}>{todo.description}</Text>
</Box>
</Box>
);
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 }) => (
<Box flexDirection="row">
<Box marginRight={1}>
<TodoStatusDisplay status={todo.status} />
</Box>
<Box flexShrink={1}>
<Text color={theme.text.primary}>{todo.description}</Text>
</Box>
</Box>
);
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<TodoListDisplayProps> = ({ todos }) => (
const TodoListDisplay: React.FC<TodoListDisplayProps> = ({ todos }) => (
<Box flexDirection="column">
{todos.todos.map((todo: Todo, index: number) => (
<TodoItemDisplay todo={todo} key={index} />

View File

@@ -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<ToolMessageProps> = ({
/>
) : typeof resultDisplay === 'object' &&
'todos' in resultDisplay ? (
<TodoListDisplay todos={resultDisplay as TodoList} />
// display nothing, as the TodoTray will handle rendering todos
<></>
) : (
<AnsiOutputText
data={resultDisplay as AnsiOutput}

View File

@@ -1,28 +1,48 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<TodoListDisplay /> > renders a single todo item 1`] = `"☐ Single task"`;
exports[`<TodoListDisplay /> > 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[`<TodoTray /> > renders a single todo item when full view is on 1`] = `
"┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 📝 Todo:(ctrl+t to collapse) │
│ │
☐ Single task │"
`;
exports[`<TodoListDisplay /> > renders a todo list with various statuses correctly 1`] = `
"☐ Task 1
» Task 2
✓ Task 3
✗ Task 4"
exports[`<TodoTray /> > 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[`<TodoListDisplay /> > renders an empty todo list correctly 1`] = `""`;
exports[`<TodoTray /> > 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[`<TodoTray /> > renders an empty todo list when full view is on 1`] = `
"┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 📝 Todo:(ctrl+t to collapse) │
│ │"
`;
exports[`<TodoTray /> > renders null when no todos are in the history 1`] = `""`;

View File

@@ -1,22 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<TodoListDisplay /> > renders a single todo item 1`] = `"☐ Single task"`;
exports[`<TodoListDisplay /> > 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[`<TodoListDisplay /> > renders a todo list with various statuses correctly 1`] = `
"☐ Task 1
» Task 2
✓ Task 3
✗ Task 4"
`;
exports[`<TodoListDisplay /> > renders an empty todo list correctly 1`] = `""`;