mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 22:48:03 +00:00
Improve rendering of ToDo lists. (#11315)
This commit is contained in:
committed by
GitHub
parent
c71b749185
commit
9a4211b610
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TodoListDisplay } from './TodoListDisplay.js';
|
||||
import type { TodoList, TodoStatus } from '@google/gemini-cli-core';
|
||||
|
||||
describe('<TodoListDisplay />', () => {
|
||||
const terminalWidth = 80;
|
||||
|
||||
it('renders an empty todo list correctly', () => {
|
||||
const todos: TodoList = { todos: [] };
|
||||
const { lastFrame } = render(
|
||||
<TodoListDisplay todos={todos} terminalWidth={terminalWidth} />,
|
||||
);
|
||||
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} terminalWidth={terminalWidth} />,
|
||||
);
|
||||
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(
|
||||
<TodoListDisplay todos={todos} terminalWidth={40} />,
|
||||
);
|
||||
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} terminalWidth={terminalWidth} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
47
packages/cli/src/ui/components/messages/TodoListDisplay.tsx
Normal file
47
packages/cli/src/ui/components/messages/TodoListDisplay.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { Todo, TodoList, TodoStatus } from '@google/gemini-cli-core';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
export interface TodoListDisplayProps {
|
||||
todos: TodoList;
|
||||
terminalWidth: number;
|
||||
}
|
||||
const TodoStatusDisplay: React.FC<{ status: TodoStatus }> = ({ status }) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <Text color={theme.status.success}>✓</Text>;
|
||||
case 'in_progress':
|
||||
return <Text color={theme.text.accent}>»</Text>;
|
||||
case 'pending':
|
||||
return <Text color={theme.text.primary}>☐</Text>;
|
||||
case 'cancelled':
|
||||
return <Text color={theme.status.error}>✗</Text>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const TodoListDisplay: React.FC<TodoListDisplayProps> = ({
|
||||
todos,
|
||||
terminalWidth,
|
||||
}) => (
|
||||
<Box flexDirection="column" width={terminalWidth}>
|
||||
{todos.todos.map((todo: Todo, index: number) => (
|
||||
<Box key={index} flexDirection="row">
|
||||
<Box marginRight={1}>
|
||||
<TodoStatusDisplay status={todo.status} />
|
||||
</Box>
|
||||
<Box flexShrink={1}>
|
||||
<Text color={theme.text.primary}>{todo.description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
@@ -13,6 +13,7 @@ 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 './TodoListDisplay.js';
|
||||
import { ShellInputPrompt } from '../ShellInputPrompt.js';
|
||||
import {
|
||||
SHELL_COMMAND_NAME,
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
TOOL_STATUS,
|
||||
} from '../../constants.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import type { AnsiOutput, Config } from '@google/gemini-cli-core';
|
||||
import type { AnsiOutput, Config, TodoList } from '@google/gemini-cli-core';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
|
||||
const STATIC_HEIGHT = 1;
|
||||
@@ -170,6 +171,12 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
availableTerminalHeight={availableHeight}
|
||||
terminalWidth={childWidth}
|
||||
/>
|
||||
) : typeof resultDisplay === 'object' &&
|
||||
'todos' in resultDisplay ? (
|
||||
<TodoListDisplay
|
||||
todos={resultDisplay as TodoList}
|
||||
terminalWidth={childWidth}
|
||||
/>
|
||||
) : (
|
||||
<AnsiOutputText
|
||||
data={resultDisplay as AnsiOutput}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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`] = `""`;
|
||||
@@ -541,7 +541,18 @@ export function hasCycleInSchema(schema: object): boolean {
|
||||
return traverse(schema, new Set<string>(), new Set<string>());
|
||||
}
|
||||
|
||||
export type ToolResultDisplay = string | FileDiff | AnsiOutput;
|
||||
export interface TodoList {
|
||||
todos: Todo[];
|
||||
}
|
||||
|
||||
export type ToolResultDisplay = string | FileDiff | AnsiOutput | TodoList;
|
||||
|
||||
export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
|
||||
|
||||
export interface Todo {
|
||||
description: string;
|
||||
status: TodoStatus;
|
||||
}
|
||||
|
||||
export interface FileDiff {
|
||||
fileDiff: string;
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('WriteTodosTool', () => {
|
||||
};
|
||||
const result = await tool.buildAndExecute(params, signal);
|
||||
expect(result.llmContent).toBe('Successfully cleared the todo list.');
|
||||
expect(result.returnDisplay).toBe('Successfully cleared the todo list.');
|
||||
expect(result.returnDisplay).toEqual({ todos: [] });
|
||||
});
|
||||
|
||||
it('should return a formatted todo list on success', async () => {
|
||||
@@ -103,7 +103,7 @@ describe('WriteTodosTool', () => {
|
||||
2. [in_progress] Second task
|
||||
3. [pending] Third task`;
|
||||
expect(result.llmContent).toBe(expectedOutput);
|
||||
expect(result.returnDisplay).toBe(expectedOutput);
|
||||
expect(result.returnDisplay).toEqual(params);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
BaseDeclarativeTool,
|
||||
BaseToolInvocation,
|
||||
Kind,
|
||||
type Todo,
|
||||
type ToolResult,
|
||||
} from './tools.js';
|
||||
import { WRITE_TODOS_TOOL_NAME } from './tool-names.js';
|
||||
@@ -79,13 +80,6 @@ The agent did not use the todo list because this task could be completed by a ti
|
||||
</example>
|
||||
`;
|
||||
|
||||
export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
|
||||
|
||||
export interface Todo {
|
||||
description: string;
|
||||
status: TodoStatus;
|
||||
}
|
||||
|
||||
export interface WriteTodosToolParams {
|
||||
/**
|
||||
* The full list of todos. This will overwrite any existing list.
|
||||
@@ -123,7 +117,7 @@ class WriteTodosToolInvocation extends BaseToolInvocation<
|
||||
|
||||
return {
|
||||
llmContent,
|
||||
returnDisplay: llmContent,
|
||||
returnDisplay: { todos },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user