Apply new style to Todos (#11607)

This commit is contained in:
Tommaso Sciortino
2025-10-21 15:40:45 -07:00
committed by GitHub
parent 16f5f767b2
commit 519bd57e55
5 changed files with 215 additions and 193 deletions
+2 -2
View File
@@ -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)"
`;