/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import stripAnsi from 'strip-ansi';
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 TodoTitleDisplay: React.FC<{
todos: TodoList;
fileName?: string | null;
}> = ({ todos, fileName }) => {
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} completed`;
}, [todos]);
return (
{fileName ? `Plan: ${stripAnsi(fileName)}` : 'Todo'}
{score} (ctrl+t to toggle)
);
};
const TodoStatusDisplay: React.FC<{ status: TodoStatus }> = ({ status }) => {
switch (status) {
case 'completed':
return (
โ
);
case 'in_progress':
return (
ยป
);
case 'pending':
return (
โ
);
case 'cancelled':
default:
return (
โ
);
}
};
const TodoItemDisplay: React.FC<{
todo: Todo;
wrap?: 'truncate';
role?: 'listitem';
}> = ({ todo, wrap, role: ariaRole }) => {
const textColor = (() => {
switch (todo.status) {
case 'in_progress':
return theme.text.accent;
case 'completed':
case 'cancelled':
return theme.text.secondary;
default:
return theme.text.primary;
}
})();
const strikethrough = todo.status === 'cancelled';
return (
{todo.description}
);
};
export const TodoTray: React.FC = () => {
const uiState = useUIState();
const todos: TodoList | null = useMemo(() => {
if (uiState.planTodos && uiState.planTodos.length > 0) {
return { todos: uiState.planTodos };
}
// 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;
}
}
return null;
}, [uiState.history, uiState.planTodos]);
const inProgress: Todo | null = useMemo(() => {
if (todos === null) {
return null;
}
return todos.todos.find((todo) => todo.status === 'in_progress') || null;
}, [todos]);
const hasActiveTodos = useMemo(() => {
if (!todos || !todos.todos) return false;
return todos.todos.some(
(todo) => todo.status === 'pending' || todo.status === 'in_progress',
);
}, [todos]);
if (
todos === null ||
!todos.todos ||
todos.todos.length === 0 ||
(!uiState.showFullTodos && !hasActiveTodos)
) {
return null;
}
const isPlan = uiState.planTodos && uiState.planTodos.length > 0;
return (
{uiState.showFullTodos ? (
) : (
{inProgress && (
)}
)}
);
};
interface TodoListDisplayProps {
todos: TodoList;
}
const TodoListDisplay: React.FC = ({ todos }) => (
{todos.todos.map((todo: Todo, index: number) => (
))}
);