mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
Create Todo List Tab (#11430)
This commit is contained in:
committed by
GitHub
parent
2ef38065c7
commit
cd76b0b22d
@@ -46,6 +46,7 @@ export enum Command {
|
|||||||
|
|
||||||
// App level bindings
|
// App level bindings
|
||||||
SHOW_ERROR_DETAILS = 'showErrorDetails',
|
SHOW_ERROR_DETAILS = 'showErrorDetails',
|
||||||
|
SHOW_FULL_TODOS = 'showFullTodos',
|
||||||
TOGGLE_IDE_CONTEXT_DETAIL = 'toggleIDEContextDetail',
|
TOGGLE_IDE_CONTEXT_DETAIL = 'toggleIDEContextDetail',
|
||||||
TOGGLE_MARKDOWN = 'toggleMarkdown',
|
TOGGLE_MARKDOWN = 'toggleMarkdown',
|
||||||
QUIT = 'quit',
|
QUIT = 'quit',
|
||||||
@@ -156,6 +157,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
|||||||
|
|
||||||
// App level bindings
|
// App level bindings
|
||||||
[Command.SHOW_ERROR_DETAILS]: [{ key: 'o', ctrl: true }],
|
[Command.SHOW_ERROR_DETAILS]: [{ key: 'o', ctrl: true }],
|
||||||
|
[Command.SHOW_FULL_TODOS]: [{ key: 't', ctrl: true }],
|
||||||
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: [{ key: 'g', ctrl: true }],
|
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: [{ key: 'g', ctrl: true }],
|
||||||
[Command.TOGGLE_MARKDOWN]: [{ key: 'm', command: true }],
|
[Command.TOGGLE_MARKDOWN]: [{ key: 'm', command: true }],
|
||||||
[Command.QUIT]: [{ key: 'c', ctrl: true }],
|
[Command.QUIT]: [{ key: 'c', ctrl: true }],
|
||||||
|
|||||||
@@ -788,6 +788,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
|
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
|
||||||
|
const [showFullTodos, setShowFullTodos] = useState<boolean>(false);
|
||||||
const [renderMarkdown, setRenderMarkdown] = useState<boolean>(true);
|
const [renderMarkdown, setRenderMarkdown] = useState<boolean>(true);
|
||||||
|
|
||||||
const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false);
|
const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false);
|
||||||
@@ -961,6 +962,8 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
|
|
||||||
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
|
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
|
||||||
setShowErrorDetails((prev) => !prev);
|
setShowErrorDetails((prev) => !prev);
|
||||||
|
} else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) {
|
||||||
|
setShowFullTodos((prev) => !prev);
|
||||||
} else if (keyMatchers[Command.TOGGLE_MARKDOWN](key)) {
|
} else if (keyMatchers[Command.TOGGLE_MARKDOWN](key)) {
|
||||||
setRenderMarkdown((prev) => {
|
setRenderMarkdown((prev) => {
|
||||||
const newValue = !prev;
|
const newValue = !prev;
|
||||||
@@ -1129,6 +1132,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
isTrustedFolder,
|
isTrustedFolder,
|
||||||
constrainHeight,
|
constrainHeight,
|
||||||
showErrorDetails,
|
showErrorDetails,
|
||||||
|
showFullTodos,
|
||||||
filteredConsoleMessages,
|
filteredConsoleMessages,
|
||||||
ideContextState,
|
ideContextState,
|
||||||
renderMarkdown,
|
renderMarkdown,
|
||||||
@@ -1209,6 +1213,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
isTrustedFolder,
|
isTrustedFolder,
|
||||||
constrainHeight,
|
constrainHeight,
|
||||||
showErrorDetails,
|
showErrorDetails,
|
||||||
|
showFullTodos,
|
||||||
filteredConsoleMessages,
|
filteredConsoleMessages,
|
||||||
ideContextState,
|
ideContextState,
|
||||||
renderMarkdown,
|
renderMarkdown,
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
|||||||
geminiMdFileCount: 0,
|
geminiMdFileCount: 0,
|
||||||
renderMarkdown: true,
|
renderMarkdown: true,
|
||||||
filteredConsoleMessages: [],
|
filteredConsoleMessages: [],
|
||||||
|
history: [],
|
||||||
sessionStats: {
|
sessionStats: {
|
||||||
lastPromptTokenCount: 0,
|
lastPromptTokenCount: 0,
|
||||||
sessionTokenCount: 0,
|
sessionTokenCount: 0,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { useSettings } from '../contexts/SettingsContext.js';
|
|||||||
import { ApprovalMode } from '@google/gemini-cli-core';
|
import { ApprovalMode } from '@google/gemini-cli-core';
|
||||||
import { StreamingState } from '../types.js';
|
import { StreamingState } from '../types.js';
|
||||||
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
|
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
|
||||||
|
import { AnchoredTodoListDisplay } from './messages/Todo.js';
|
||||||
|
|
||||||
export const Composer = () => {
|
export const Composer = () => {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
@@ -129,6 +130,8 @@ export const Composer = () => {
|
|||||||
</OverflowProvider>
|
</OverflowProvider>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<AnchoredTodoListDisplay />
|
||||||
|
|
||||||
{uiState.isInputActive && (
|
{uiState.isInputActive && (
|
||||||
<InputPrompt
|
<InputPrompt
|
||||||
buffer={uiState.buffer}
|
buffer={uiState.buffer}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from 'ink-testing-library';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Box } from 'ink';
|
||||||
|
import { AnchoredTodoListDisplay, TodoListDisplay } from './Todo.js';
|
||||||
|
import type { TodoList, TodoStatus } 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('<AnchoredTodoListDisplay />', () => {
|
||||||
|
const mockHistoryItem = {
|
||||||
|
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: 'in_progress' },
|
||||||
|
{ description: 'Completed Task', status: 'completed' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as HistoryItem;
|
||||||
|
|
||||||
|
const renderWithUiState = (uiState: Partial<UIState>) =>
|
||||||
|
render(
|
||||||
|
<UIStateContext.Provider value={uiState as UIState}>
|
||||||
|
<AnchoredTodoListDisplay />
|
||||||
|
</UIStateContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
it('renders null when no todos are in the history', () => {
|
||||||
|
const { lastFrame } = renderWithUiState({ history: [] });
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
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 { lastFrame } = renderWithUiState({
|
||||||
|
history: [historyWithNoInProgress],
|
||||||
|
showFullTodos: false,
|
||||||
|
});
|
||||||
|
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],
|
||||||
|
showFullTodos: true,
|
||||||
|
});
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import {
|
||||||
|
type Todo,
|
||||||
|
type TodoList,
|
||||||
|
type TodoStatus,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
|
import { theme } from '../../semantic-colors.js';
|
||||||
|
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':
|
||||||
|
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':
|
||||||
|
default:
|
||||||
|
return <Text color={theme.status.error}>✗</Text>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AnchoredTodoListDisplay: React.FC = () => {
|
||||||
|
const uiState = useUIState();
|
||||||
|
|
||||||
|
const todos: TodoList | null = useMemo(() => {
|
||||||
|
// Find the most recent todo list written by the WriteTodosTool
|
||||||
|
for (let i = uiState.history.length - 1; i >= 0; i--) {
|
||||||
|
const entry = uiState.history[i];
|
||||||
|
if (entry.type !== 'tool_group') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const toolGroup = entry as HistoryItemToolGroup;
|
||||||
|
for (const tool of toolGroup.tools) {
|
||||||
|
if (
|
||||||
|
typeof tool.resultDisplay !== 'object' ||
|
||||||
|
!('todos' in tool.resultDisplay)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return tool.resultDisplay as TodoList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [uiState.history]);
|
||||||
|
|
||||||
|
const inProgress: Todo | null = useMemo(() => {
|
||||||
|
if (todos === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return todos.todos.find((todo) => todo.status === 'in_progress') || null;
|
||||||
|
}, [todos]);
|
||||||
|
|
||||||
|
if (todos === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.showFullTodos) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
borderStyle="single"
|
||||||
|
paddingLeft={1}
|
||||||
|
paddingRight={1}
|
||||||
|
borderBottom={false}
|
||||||
|
flexDirection="column"
|
||||||
|
borderColor={theme.border.default}
|
||||||
|
>
|
||||||
|
<Text color={theme.text.accent}>
|
||||||
|
📝 Todo:
|
||||||
|
<Text color={theme.text.secondary}>(ctrl+t to collapse)</Text>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Box paddingLeft={4} paddingRight={2} paddingTop={1}>
|
||||||
|
<TodoListDisplay todos={todos!} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inProgress === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
borderStyle="single"
|
||||||
|
paddingLeft={1}
|
||||||
|
paddingRight={1}
|
||||||
|
borderBottom={false}
|
||||||
|
flexDirection="row"
|
||||||
|
borderColor={theme.border.default}
|
||||||
|
>
|
||||||
|
<Text color={theme.text.accent}>
|
||||||
|
📝 Todo:
|
||||||
|
<Text color={theme.text.secondary}>(ctrl+t to expand)</Text>
|
||||||
|
</Text>
|
||||||
|
<TodoItemDisplay todo={inProgress} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface TodoListDisplayProps {
|
||||||
|
todos: TodoList;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TodoListDisplay: React.FC<TodoListDisplayProps> = ({ todos }) => (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
{todos.todos.map((todo: Todo, index: number) => (
|
||||||
|
<TodoItemDisplay todo={todo} key={index} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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,7 +13,7 @@ import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
|||||||
import { AnsiOutputText } from '../AnsiOutput.js';
|
import { AnsiOutputText } from '../AnsiOutput.js';
|
||||||
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
|
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
|
||||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||||
import { TodoListDisplay } from './TodoListDisplay.js';
|
import { TodoListDisplay } from './Todo.js';
|
||||||
import { ShellInputPrompt } from '../ShellInputPrompt.js';
|
import { ShellInputPrompt } from '../ShellInputPrompt.js';
|
||||||
import {
|
import {
|
||||||
SHELL_COMMAND_NAME,
|
SHELL_COMMAND_NAME,
|
||||||
@@ -173,10 +173,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||||||
/>
|
/>
|
||||||
) : typeof resultDisplay === 'object' &&
|
) : typeof resultDisplay === 'object' &&
|
||||||
'todos' in resultDisplay ? (
|
'todos' in resultDisplay ? (
|
||||||
<TodoListDisplay
|
<TodoListDisplay todos={resultDisplay as TodoList} />
|
||||||
todos={resultDisplay as TodoList}
|
|
||||||
terminalWidth={childWidth}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<AnsiOutputText
|
<AnsiOutputText
|
||||||
data={resultDisplay as AnsiOutput}
|
data={resultDisplay as AnsiOutput}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`<AnchoredTodoListDisplay /> > renders null when no todos are in the history 1`] = `""`;
|
||||||
|
|
||||||
|
exports[`<AnchoredTodoListDisplay /> > renders null when todos exist but none are in progress and full view is off 1`] = `""`;
|
||||||
|
|
||||||
|
exports[`<AnchoredTodoListDisplay /> > renders only the in-progress task when full view is off 1`] = `
|
||||||
|
"┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 📝 Todo:(ctrl+t to expand)» In Progress Task │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<AnchoredTodoListDisplay /> > renders the full todo list when full view is on 1`] = `
|
||||||
|
"┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 📝 Todo:(ctrl+t to collapse) │
|
||||||
|
│ │
|
||||||
|
│ ☐ Pending Task │
|
||||||
|
│ » In Progress Task │
|
||||||
|
│ ✓ Completed Task │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
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`] = `""`;
|
||||||
@@ -120,6 +120,7 @@ export interface UIState {
|
|||||||
activePtyId: number | undefined;
|
activePtyId: number | undefined;
|
||||||
embeddedShellFocused: boolean;
|
embeddedShellFocused: boolean;
|
||||||
showDebugProfiler: boolean;
|
showDebugProfiler: boolean;
|
||||||
|
showFullTodos: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UIStateContext = createContext<UIState | null>(null);
|
export const UIStateContext = createContext<UIState | null>(null);
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ describe('keyMatchers', () => {
|
|||||||
key.ctrl && (key.name === 'x' || key.sequence === '\x18'),
|
key.ctrl && (key.name === 'x' || key.sequence === '\x18'),
|
||||||
[Command.PASTE_CLIPBOARD_IMAGE]: (key: Key) => key.ctrl && key.name === 'v',
|
[Command.PASTE_CLIPBOARD_IMAGE]: (key: Key) => key.ctrl && key.name === 'v',
|
||||||
[Command.SHOW_ERROR_DETAILS]: (key: Key) => key.ctrl && key.name === 'o',
|
[Command.SHOW_ERROR_DETAILS]: (key: Key) => key.ctrl && key.name === 'o',
|
||||||
|
[Command.SHOW_FULL_TODOS]: (key: Key) => key.ctrl && key.name === 't',
|
||||||
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: (key: Key) =>
|
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: (key: Key) =>
|
||||||
key.ctrl && key.name === 'g',
|
key.ctrl && key.name === 'g',
|
||||||
[Command.TOGGLE_MARKDOWN]: (key: Key) => key.meta && key.name === 'm',
|
[Command.TOGGLE_MARKDOWN]: (key: Key) => key.meta && key.name === 'm',
|
||||||
@@ -214,6 +215,11 @@ describe('keyMatchers', () => {
|
|||||||
positive: [createKey('o', { ctrl: true })],
|
positive: [createKey('o', { ctrl: true })],
|
||||||
negative: [createKey('o'), createKey('e', { ctrl: true })],
|
negative: [createKey('o'), createKey('e', { ctrl: true })],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
command: Command.SHOW_FULL_TODOS,
|
||||||
|
positive: [createKey('t', { ctrl: true })],
|
||||||
|
negative: [createKey('t'), createKey('e', { ctrl: true })],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
command: Command.TOGGLE_IDE_CONTEXT_DETAIL,
|
command: Command.TOGGLE_IDE_CONTEXT_DETAIL,
|
||||||
positive: [createKey('g', { ctrl: true })],
|
positive: [createKey('g', { ctrl: true })],
|
||||||
|
|||||||
Reference in New Issue
Block a user