// ***************************************************************************** // Copyright (C) 2025 EclipseSource GmbH. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { ChatResponsePartRenderer } from '@theia/ai-chat-ui/lib/browser/chat-response-part-renderer'; import { ResponseNode } from '@theia/ai-chat-ui/lib/browser/chat-tree-view'; import { ChatResponseContent, ToolCallChatResponseContent } from '@theia/ai-chat/lib/common'; import { codicon } from '@theia/core/lib/browser'; import { injectable } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; import { ReactNode } from '@theia/core/shared/react'; import { ClaudeCodeToolCallChatResponseContent } from '../claude-code-tool-call-content'; import { CollapsibleToolRenderer } from './collapsible-tool-renderer'; import { nls } from '@theia/core'; interface TodoItem { id: string; content: string; status: 'pending' | 'in_progress' | 'completed'; priority: keyof typeof TODO_PRIORITIES; } const TODO_PRIORITIES = { 'high': nls.localize('theia/ai/claude-code/todoPriority/high', 'high'), 'medium': nls.localize('theia/ai/claude-code/todoPriority/medium', 'medium'), 'low': nls.localize('theia/ai/claude-code/todoPriority/low', 'low') }; interface TodoWriteInput { todos: TodoItem[]; } // Session-scoped registry to track TodoWrite renderer instances per session class TodoWriteRegistry { private static sessionInstances: Map void>> = new Map(); static register(sessionId: string, hideFn: () => void): void { // Get or create instances set for this session let sessionSet = this.sessionInstances.get(sessionId); if (!sessionSet) { sessionSet = new Set(); this.sessionInstances.set(sessionId, sessionSet); } // Hide all previous instances in this session sessionSet.forEach(fn => fn()); // Clear the session registry sessionSet.clear(); // Add the new instance sessionSet.add(hideFn); } static unregister(sessionId: string, hideFn: () => void): void { const sessionSet = this.sessionInstances.get(sessionId); if (sessionSet) { sessionSet.delete(hideFn); // Clean up empty session entries if (sessionSet.size === 0) { this.sessionInstances.delete(sessionId); } } } } @injectable() export class TodoWriteRenderer implements ChatResponsePartRenderer { canHandle(response: ChatResponseContent): number { if (ClaudeCodeToolCallChatResponseContent.is(response) && response.name === 'TodoWrite') { return 15; // Higher than default ToolCallPartRenderer (10) } return -1; } render(response: ToolCallChatResponseContent, parentNode: ResponseNode): ReactNode { try { const input = JSON.parse(response.arguments || '{}') as TodoWriteInput; return ; } catch (error) { console.warn('Failed to parse TodoWrite input:', error); return
{nls.localize('theia/ai/claude-code/failedToParseTodoListData', 'Failed to parse todo list data')}
; } } } const TodoListComponent: React.FC<{ todos: TodoItem[]; sessionId: string }> = ({ todos, sessionId }) => { const [isHidden, setIsHidden] = React.useState(false); React.useEffect(() => { const hideFn = () => setIsHidden(true); TodoWriteRegistry.register(sessionId, hideFn); return () => { TodoWriteRegistry.unregister(sessionId, hideFn); }; }, [sessionId]); if (isHidden) { // eslint-disable-next-line no-null/no-null return null; } const getStatusIcon = (status: TodoItem['status']) => { switch (status) { case 'completed': return ; case 'in_progress': return ; case 'pending': default: return ; } }; const getPriorityBadge = (priority: TodoItem['priority']) => ( {TODO_PRIORITIES[priority]} ); if (!todos || todos.length === 0) { return (
{nls.localize('theia/ai/claude-code/todoList', 'Todo List')}
{nls.localize('theia/ai/claude-code/emptyTodoList', 'No todos available')}
); } const completedCount = todos.filter(todo => todo.status === 'completed').length; const totalCount = todos.length; const compactHeader = ( <>
{nls.localize('theia/ai/claude-code/todoList', 'Todo List')} {nls.localize('theia/ai/claude-code/completedCount', '{0}/{1} completed', completedCount, totalCount)}
{totalCount === 1 ? nls.localize('theia/ai/claude-code/oneItem', '1 item') : nls.localize('theia/ai/claude-code/itemCount', '{0} items', totalCount)}
); const expandedContent = (
{todos.map(todo => (
{getStatusIcon(todo.status)}
{todo.content} {getPriorityBadge(todo.priority)}
))}
); return ( ); };