mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 10:34:35 -07:00
Apply new style to Todos (#11607)
This commit is contained in:
committed by
GitHub
parent
16f5f767b2
commit
519bd57e55
@@ -66,6 +66,8 @@ export const Composer = () => {
|
||||
|
||||
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
|
||||
|
||||
<TodoTray />
|
||||
|
||||
<Box
|
||||
marginTop={1}
|
||||
justifyContent={
|
||||
@@ -130,8 +132,6 @@ export const Composer = () => {
|
||||
</OverflowProvider>
|
||||
)}
|
||||
|
||||
<TodoTray />
|
||||
|
||||
{uiState.isInputActive && (
|
||||
<InputPrompt
|
||||
buffer={uiState.buffer}
|
||||
|
||||
@@ -31,12 +31,6 @@ const createTodoHistoryItem = (todos: Todo[]): 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(
|
||||
<UIStateContext.Provider value={uiState as UIState}>
|
||||
@@ -44,99 +38,106 @@ describe('<TodoTray />', () => {
|
||||
</UIStateContext.Provider>,
|
||||
);
|
||||
|
||||
it('renders null when no todos are in the history', () => {
|
||||
const { lastFrame } = renderWithUiState({ history: [] });
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
it.each([true, false])(
|
||||
'renders null when no todos are in the history',
|
||||
(showFullTodos) => {
|
||||
const { lastFrame } = renderWithUiState({ history: [], showFullTodos });
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
|
||||
it('renders null when todos exist but none are in progress and full view is off', () => {
|
||||
const historyWithNoInProgress = createTodoHistoryItem([
|
||||
{ description: 'Pending Task', status: 'pending' },
|
||||
{ description: 'In Progress Task', status: 'cancelled' },
|
||||
{ description: 'Completed Task', status: 'completed' },
|
||||
]);
|
||||
it.each([true, false])(
|
||||
'renders null when todo list is empty',
|
||||
(showFullTodos) => {
|
||||
const { lastFrame } = renderWithUiState({
|
||||
history: [createTodoHistoryItem([])],
|
||||
showFullTodos,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([true, false])(
|
||||
'renders when todos exist but none are in progress',
|
||||
(showFullTodos) => {
|
||||
const { lastFrame } = renderWithUiState({
|
||||
history: [
|
||||
createTodoHistoryItem([
|
||||
{ description: 'Pending Task', status: 'pending' },
|
||||
{ description: 'In Progress Task', status: 'cancelled' },
|
||||
{ description: 'Completed Task', status: 'completed' },
|
||||
]),
|
||||
],
|
||||
showFullTodos,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([true, false])(
|
||||
'renders when todos exist and one is in progress',
|
||||
(showFullTodos) => {
|
||||
const { lastFrame } = renderWithUiState({
|
||||
history: [
|
||||
createTodoHistoryItem([
|
||||
{ description: 'Pending Task', status: 'pending' },
|
||||
{ description: 'Task 2', status: 'in_progress' },
|
||||
{ description: 'In Progress Task', status: 'cancelled' },
|
||||
{ description: 'Completed Task', status: 'completed' },
|
||||
]),
|
||||
],
|
||||
showFullTodos,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([true, false])(
|
||||
'renders a todo list with long descriptions that wrap when full view is on',
|
||||
(showFullTodos) => {
|
||||
const { lastFrame } = render(
|
||||
<Box width="50">
|
||||
<UIStateContext.Provider
|
||||
value={
|
||||
{
|
||||
history: [
|
||||
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: 'in_progress',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Another completed task with an equally verbose description to test wrapping behavior.',
|
||||
status: 'completed',
|
||||
},
|
||||
]),
|
||||
],
|
||||
showFullTodos,
|
||||
} as UIState
|
||||
}
|
||||
>
|
||||
<TodoTray />
|
||||
</UIStateContext.Provider>
|
||||
</Box>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
|
||||
it('renders the most recent todo list when multiple write_todos calls are in history', () => {
|
||||
const { lastFrame } = renderWithUiState({
|
||||
history: [historyWithNoInProgress],
|
||||
showFullTodos: false,
|
||||
});
|
||||
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],
|
||||
showFullTodos: false,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders the full todo list when full view is on', () => {
|
||||
const { lastFrame } = renderWithUiState({
|
||||
history: [mockHistoryItem],
|
||||
history: [
|
||||
createTodoHistoryItem([
|
||||
{ description: 'Older Task 1', status: 'completed' },
|
||||
{ description: 'Older Task 2', status: 'pending' },
|
||||
]),
|
||||
createTodoHistoryItem([
|
||||
{ description: 'Newer Task 1', status: 'pending' },
|
||||
{ description: 'Newer Task 2', status: 'in_progress' },
|
||||
]),
|
||||
],
|
||||
showFullTodos: true,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
|
||||
@@ -16,6 +16,31 @@ import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { useMemo } from 'react';
|
||||
import type { HistoryItemToolGroup } from '../../types.js';
|
||||
|
||||
const TodoTitleDisplay: React.FC<{ todos: TodoList }> = ({ todos }) => {
|
||||
const score = useMemo(() => {
|
||||
let total = 0;
|
||||
let completed = 0;
|
||||
for (const todo of todos.todos) {
|
||||
if (todo.status !== 'cancelled') {
|
||||
total += 1;
|
||||
if (todo.status === 'completed') {
|
||||
completed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return `${completed}/${total}`;
|
||||
}, [todos]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" columnGap={2} height={1}>
|
||||
<Text color={theme.text.primary} bold>
|
||||
📝 Todo
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>{score} (ctrl+t to toggle)</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const TodoStatusDisplay: React.FC<{ status: TodoStatus }> = ({ status }) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
@@ -30,13 +55,16 @@ 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>
|
||||
const TodoItemDisplay: React.FC<{ todo: Todo; wrap?: 'truncate' }> = ({
|
||||
todo,
|
||||
wrap,
|
||||
}) => (
|
||||
<Box flexDirection="row" columnGap={1}>
|
||||
<TodoStatusDisplay status={todo.status} />
|
||||
<Box flexShrink={1}>
|
||||
<Text color={theme.text.primary}>{todo.description}</Text>
|
||||
<Text color={theme.text.primary} wrap={wrap}>
|
||||
{todo.description}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -72,50 +100,37 @@ export const TodoTray: React.FC = () => {
|
||||
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) {
|
||||
if (todos === null || !todos.todos || todos.todos.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderBottom={false}
|
||||
borderRight={false}
|
||||
borderLeft={false}
|
||||
borderColor={theme.border.default}
|
||||
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} />
|
||||
{uiState.showFullTodos ? (
|
||||
<Box flexDirection="column" rowGap={1}>
|
||||
<TodoTitleDisplay todos={todos} />
|
||||
<TodoListDisplay todos={todos!} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box flexDirection="row" columnGap={1} height={1}>
|
||||
<Box flexShrink={0} flexGrow={0}>
|
||||
<TodoTitleDisplay todos={todos} />
|
||||
</Box>
|
||||
{inProgress && (
|
||||
<Box flexShrink={1} flexGrow={1}>
|
||||
<TodoItemDisplay todo={inProgress!} wrap="truncate" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,63 +1,62 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<TodoTray /> > renders a single todo item when full view is on 1`] = `
|
||||
"┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 📝 Todo:(ctrl+t to collapse) │
|
||||
│ │
|
||||
│ ☐ Single task │"
|
||||
`;
|
||||
|
||||
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. │"
|
||||
"──────────────────────────────────────────────────
|
||||
📝 Todo 1/2 (ctrl+t to toggle)
|
||||
|
||||
» 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 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 a todo list with long descriptions that wrap when full view is on 2`] = `
|
||||
"──────────────────────────────────────────────────
|
||||
📝 Todo 1/2 (ctrl+t to toggle) » This is a ver…"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> > renders null when no todos are in the history 1`] = `""`;
|
||||
|
||||
exports[`<TodoTray /> > renders null when todos exist but none are in progress and full view is off 1`] = `""`;
|
||||
exports[`<TodoTray /> > renders null when no todos are in the history 2`] = `""`;
|
||||
|
||||
exports[`<TodoTray /> > renders only the in-progress task when full view is off 1`] = `
|
||||
"┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 📝 Todo:(ctrl+t to expand)» In Progress Task │"
|
||||
exports[`<TodoTray /> > renders null when todo list is empty 1`] = `""`;
|
||||
|
||||
exports[`<TodoTray /> > renders null when todo list is empty 2`] = `""`;
|
||||
|
||||
exports[`<TodoTray /> > renders the most recent todo list when multiple write_todos calls are in history 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
📝 Todo 0/2 (ctrl+t to toggle)
|
||||
|
||||
☐ Newer Task 1
|
||||
» Newer Task 2"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> > renders the full todo list when full view is on 1`] = `
|
||||
"┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 📝 Todo:(ctrl+t to collapse) │
|
||||
│ │
|
||||
│ ☐ Pending Task │
|
||||
│ » In Progress Task │
|
||||
│ ✓ Completed Task │"
|
||||
exports[`<TodoTray /> > renders when todos exist and one is in progress 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
📝 Todo 1/3 (ctrl+t to toggle)
|
||||
|
||||
☐ Pending Task
|
||||
» Task 2
|
||||
✗ In Progress Task
|
||||
✓ Completed Task"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> > renders when todos exist and one is in progress 2`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
📝 Todo 1/3 (ctrl+t to toggle) » Task 2"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> > renders when todos exist but none are in progress 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
📝 Todo 1/2 (ctrl+t to toggle)
|
||||
|
||||
☐ Pending Task
|
||||
✗ In Progress Task
|
||||
✓ Completed Task"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> > renders when todos exist but none are in progress 2`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
📝 Todo 1/2 (ctrl+t to toggle)"
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user