import { Box, Text } from 'ink' import * as React from 'react' import { z } from 'zod' import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage' import { TodoItem as TodoItemComponent } from '../../components/TodoItem' import { Tool, ValidationResult } from '../../Tool' import { setTodos, getTodos, TodoItem } from '../../utils/todoStorage' import { emitReminderEvent } from '../../services/systemReminder' import { startWatchingTodoFile } from '../../services/fileFreshness' import { DESCRIPTION, PROMPT } from './prompt' import { getTheme } from '../../utils/theme' const TodoItemSchema = z.object({ content: z.string().min(1).describe('The task description or content'), status: z .enum(['pending', 'in_progress', 'completed']) .describe('Current status of the task'), priority: z .enum(['high', 'medium', 'low']) .describe('Priority level of the task'), id: z.string().min(1).describe('Unique identifier for the task'), }) const inputSchema = z.strictObject({ todos: z.array(TodoItemSchema).describe('The updated todo list'), }) function validateTodos(todos: TodoItem[]): ValidationResult { // Check for duplicate IDs const ids = todos.map(todo => todo.id) const uniqueIds = new Set(ids) if (ids.length !== uniqueIds.size) { return { result: false, errorCode: 1, message: 'Duplicate todo IDs found', meta: { duplicateIds: ids.filter((id, index) => ids.indexOf(id) !== index), }, } } // Check for multiple in_progress tasks const inProgressTasks = todos.filter(todo => todo.status === 'in_progress') if (inProgressTasks.length > 1) { return { result: false, errorCode: 2, message: 'Only one task can be in_progress at a time', meta: { inProgressTaskIds: inProgressTasks.map(t => t.id) }, } } // Validate each todo for (const todo of todos) { if (!todo.content?.trim()) { return { result: false, errorCode: 3, message: `Todo with ID "${todo.id}" has empty content`, meta: { todoId: todo.id }, } } if (!['pending', 'in_progress', 'completed'].includes(todo.status)) { return { result: false, errorCode: 4, message: `Invalid status "${todo.status}" for todo "${todo.id}"`, meta: { todoId: todo.id, invalidStatus: todo.status }, } } if (!['high', 'medium', 'low'].includes(todo.priority)) { return { result: false, errorCode: 5, message: `Invalid priority "${todo.priority}" for todo "${todo.id}"`, meta: { todoId: todo.id, invalidPriority: todo.priority }, } } } return { result: true } } function generateTodoSummary(todos: TodoItem[]): string { const stats = { total: todos.length, pending: todos.filter(t => t.status === 'pending').length, inProgress: todos.filter(t => t.status === 'in_progress').length, completed: todos.filter(t => t.status === 'completed').length, } // Enhanced summary with statistics let summary = `Updated ${stats.total} todo(s)` if (stats.total > 0) { summary += ` (${stats.pending} pending, ${stats.inProgress} in progress, ${stats.completed} completed)` } summary += '. Continue tracking your progress with the todo list.' return summary } export const TodoWriteTool = { name: 'TodoWrite', async description() { return DESCRIPTION }, async prompt() { return PROMPT }, inputSchema, userFacingName() { return 'Update Todos' }, async isEnabled() { return true }, isReadOnly() { return false }, isConcurrencySafe() { return false // TodoWrite modifies state, not safe for concurrent execution }, needsPermissions() { return false }, renderResultForAssistant(result) { // Match official implementation - return static confirmation message return 'Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable' }, renderToolUseMessage(input, { verbose }) { // Show a simple confirmation message when the tool is being used return '{ params.todo }' }, renderToolUseRejectedMessage() { return }, renderToolResultMessage(output) { const isError = typeof output === 'string' && output.startsWith('Error') // For non-error output, get current todos from storage and render them if (!isError && typeof output === 'string') { const currentTodos = getTodos() if (currentTodos.length === 0) { return (   ⎿   No todos currently ) } // Sort: [completed, in_progress, pending] const sortedTodos = [...currentTodos].sort((a, b) => { const order = ['completed', 'in_progress', 'pending'] return ( order.indexOf(a.status) - order.indexOf(b.status) || a.content.localeCompare(b.content) ) }) // Find the next pending task (first pending task after sorting) const nextPendingIndex = sortedTodos.findIndex(todo => todo.status === 'pending') return ( {sortedTodos.map((todo: TodoItem, index: number) => { // Determine checkbox symbol and colors let checkbox: string let textColor: string let isBold = false let isStrikethrough = false if (todo.status === 'completed') { checkbox = '☒' textColor = '#6B7280' // Professional gray for completed isStrikethrough = true } else if (todo.status === 'in_progress') { checkbox = '☐' textColor = '#10B981' // Professional green for in progress isBold = true } else if (todo.status === 'pending') { checkbox = '☐' // Only the FIRST pending task gets purple highlight if (index === nextPendingIndex) { textColor = '#8B5CF6' // Professional purple for next pending isBold = true } else { textColor = '#9CA3AF' // Muted gray for other pending } } return (   ⎿   {checkbox} {todo.content} ) })} ) } // Fallback to simple text rendering for errors or string output return (   ⎿   {typeof output === 'string' ? output : JSON.stringify(output)} ) }, async validateInput({ todos }: z.infer) { // Type assertion to ensure todos match TodoItem[] interface const todoItems = todos as TodoItem[] const validation = validateTodos(todoItems) if (!validation.result) { return validation } return { result: true } }, async *call({ todos }: z.infer, context) { try { // Get agent ID from context const agentId = context?.agentId // Start watching todo file for this agent if not already watching if (agentId) { startWatchingTodoFile(agentId) } // Store previous todos for comparison (agent-scoped) const previousTodos = getTodos(agentId) // Type assertion to ensure todos match TodoItem[] interface const todoItems = todos as TodoItem[] // Note: Validation already done in validateInput, no need for duplicate validation // This eliminates the double validation issue // Update the todos in storage (agent-scoped) setTodos(todoItems, agentId) // Emit todo change event for system reminders (optimized - only if todos actually changed) const hasChanged = JSON.stringify(previousTodos) !== JSON.stringify(todoItems) if (hasChanged) { emitReminderEvent('todo:changed', { previousTodos, newTodos: todoItems, timestamp: Date.now(), agentId: agentId || 'default', changeType: todoItems.length > previousTodos.length ? 'added' : todoItems.length < previousTodos.length ? 'removed' : 'modified', }) } // Generate enhanced summary const summary = generateTodoSummary(todoItems) // Enhanced result data for rendering const resultData = { oldTodos: previousTodos, newTodos: todoItems, summary, } yield { type: 'result', data: summary, // Return string to satisfy interface resultForAssistant: summary, // Store todo data in a way accessible to the renderer // We'll modify the renderToolResultMessage to get todos from storage } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' const errorResult = `Error updating todos: ${errorMessage}` // Emit error event for system monitoring emitReminderEvent('todo:error', { error: errorMessage, timestamp: Date.now(), agentId: context?.agentId || 'default', context: 'TodoWriteTool.call', }) yield { type: 'result', data: errorResult, resultForAssistant: errorResult, } } }, } satisfies Tool