import type { TodoItem } from "@roo-code/types" import type { ToolData } from "../types.js" /** * Extract structured ToolData from parsed tool JSON * This provides rich data for tool-specific renderers */ export function extractToolData(toolInfo: Record): ToolData { const toolName = (toolInfo.tool as string) || "unknown" // Base tool data with common fields const toolData: ToolData = { tool: toolName, path: toolInfo.path as string | undefined, isOutsideWorkspace: toolInfo.isOutsideWorkspace as boolean | undefined, isProtected: toolInfo.isProtected as boolean | undefined, content: toolInfo.content as string | undefined, reason: toolInfo.reason as string | undefined, } // Extract diff-related fields if (toolInfo.diff !== undefined) { toolData.diff = toolInfo.diff as string } if (toolInfo.diffStats !== undefined) { const stats = toolInfo.diffStats as { added?: number; removed?: number } if (typeof stats.added === "number" && typeof stats.removed === "number") { toolData.diffStats = { added: stats.added, removed: stats.removed } } } // Extract search-related fields if (toolInfo.regex !== undefined) { toolData.regex = toolInfo.regex as string } if (toolInfo.filePattern !== undefined) { toolData.filePattern = toolInfo.filePattern as string } if (toolInfo.query !== undefined) { toolData.query = toolInfo.query as string } // Extract mode-related fields if (toolInfo.mode !== undefined) { toolData.mode = toolInfo.mode as string } if (toolInfo.mode_slug !== undefined) { toolData.mode = toolInfo.mode_slug as string } // Extract command-related fields if (toolInfo.command !== undefined) { toolData.command = toolInfo.command as string } if (toolInfo.output !== undefined) { toolData.output = toolInfo.output as string } // Extract batch file operations if (Array.isArray(toolInfo.files)) { toolData.batchFiles = (toolInfo.files as Array>).map((f) => ({ path: (f.path as string) || "", lineSnippet: f.lineSnippet as string | undefined, isOutsideWorkspace: f.isOutsideWorkspace as boolean | undefined, key: f.key as string | undefined, content: f.content as string | undefined, })) } // Extract batch diff operations if (Array.isArray(toolInfo.batchDiffs)) { toolData.batchDiffs = (toolInfo.batchDiffs as Array>).map((d) => ({ path: (d.path as string) || "", changeCount: d.changeCount as number | undefined, key: d.key as string | undefined, content: d.content as string | undefined, diffStats: d.diffStats as { added: number; removed: number } | undefined, diffs: d.diffs as Array<{ content: string; startLine?: number }> | undefined, })) } // Extract question/completion fields if (toolInfo.question !== undefined) { toolData.question = toolInfo.question as string } if (toolInfo.result !== undefined) { toolData.result = toolInfo.result as string } // Extract additional display hints if (toolInfo.lineNumber !== undefined) { toolData.lineNumber = toolInfo.lineNumber as number } if (toolInfo.additionalFileCount !== undefined) { toolData.additionalFileCount = toolInfo.additionalFileCount as number } return toolData } /** * Format tool output for display (used in the message body, header shows tool name separately) */ export function formatToolOutput(toolInfo: Record): string { const toolName = (toolInfo.tool as string) || "unknown" switch (toolName) { case "switchMode": { const mode = (toolInfo.mode as string) || "unknown" const reason = toolInfo.reason as string return `→ ${mode} mode${reason ? `\n ${reason}` : ""}` } case "switch_mode": { const mode = (toolInfo.mode_slug as string) || (toolInfo.mode as string) || "unknown" const reason = toolInfo.reason as string return `→ ${mode} mode${reason ? `\n ${reason}` : ""}` } case "execute_command": { const command = toolInfo.command as string return `$ ${command || "(no command)"}` } case "read_file": { const files = toolInfo.files as Array<{ path: string }> | undefined const path = toolInfo.path as string if (files && files.length > 0) { return files.map((f) => `📄 ${f.path}`).join("\n") } return `📄 ${path || "(no path)"}` } case "write_to_file": { const writePath = toolInfo.path as string return `📝 ${writePath || "(no path)"}` } case "apply_diff": { const diffPath = toolInfo.path as string return `✏️ ${diffPath || "(no path)"}` } case "search_files": { const searchPath = toolInfo.path as string const regex = toolInfo.regex as string return `🔍 "${regex}" in ${searchPath || "."}` } case "list_files": { const listPath = toolInfo.path as string const recursive = toolInfo.recursive as boolean return `📁 ${listPath || "."}${recursive ? " (recursive)" : ""}` } case "attempt_completion": { const result = toolInfo.result as string if (result) { const truncated = result.length > 100 ? result.substring(0, 100) + "..." : result return `✅ ${truncated}` } return "✅ Task completed" } case "ask_followup_question": { const question = toolInfo.question as string return `❓ ${question || "(no question)"}` } case "new_task": { const taskMode = toolInfo.mode as string return `📋 Creating subtask${taskMode ? ` in ${taskMode} mode` : ""}` } case "update_todo_list": case "updateTodoList": { // Special marker - actual rendering is handled by TodoChangeDisplay component return "☑ TODO list updated" } default: { const params = Object.entries(toolInfo) .filter(([key]) => key !== "tool") .map(([key, value]) => { const displayValue = typeof value === "string" ? value : JSON.stringify(value) const truncated = displayValue.length > 100 ? displayValue.substring(0, 100) + "..." : displayValue return `${key}: ${truncated}` }) .join("\n") return params || "(no parameters)" } } } /** * Format tool ask message for user approval prompt */ export function formatToolAskMessage(toolInfo: Record): string { const toolName = (toolInfo.tool as string) || "unknown" switch (toolName) { case "switchMode": case "switch_mode": { const mode = (toolInfo.mode as string) || (toolInfo.mode_slug as string) || "unknown" const reason = toolInfo.reason as string return `Switch to ${mode} mode?${reason ? `\nReason: ${reason}` : ""}` } case "execute_command": { const command = toolInfo.command as string return `Run command?\n$ ${command || "(no command)"}` } case "read_file": { const files = toolInfo.files as Array<{ path: string }> | undefined const path = toolInfo.path as string if (files && files.length > 0) { return `Read ${files.length} file(s)?\n${files.map((f) => ` ${f.path}`).join("\n")}` } return `Read file: ${path || "(no path)"}` } case "write_to_file": { const writePath = toolInfo.path as string return `Write to file: ${writePath || "(no path)"}` } case "apply_diff": { const diffPath = toolInfo.path as string return `Apply changes to: ${diffPath || "(no path)"}` } default: { const params = Object.entries(toolInfo) .filter(([key]) => key !== "tool") .map(([key, value]) => { const displayValue = typeof value === "string" ? value : JSON.stringify(value) const truncated = displayValue.length > 80 ? displayValue.substring(0, 80) + "..." : displayValue return ` ${key}: ${truncated}` }) .join("\n") return `${toolName}${params ? `\n${params}` : ""}` } } } /** * Parse TODO items from tool info * Handles both array format and markdown checklist string format */ export function parseTodosFromToolInfo(toolInfo: Record): TodoItem[] | null { // Try to get todos directly as an array const todosArray = toolInfo.todos as unknown[] | undefined if (Array.isArray(todosArray)) { return todosArray .map((item, index) => { if (typeof item === "object" && item !== null) { const todo = item as Record return { id: (todo.id as string) || `todo-${index}`, content: (todo.content as string) || "", status: ((todo.status as string) || "pending") as TodoItem["status"], } } return null }) .filter((item): item is TodoItem => item !== null) } // Try to parse markdown checklist format from todos string const todosString = toolInfo.todos as string | undefined if (typeof todosString === "string") { return parseMarkdownChecklist(todosString) } return null } /** * Parse a markdown checklist string into TodoItem array * Format: * [ ] pending item * [-] in progress item * [x] completed item */ export function parseMarkdownChecklist(markdown: string): TodoItem[] { const lines = markdown.split("\n") const todos: TodoItem[] = [] for (let i = 0; i < lines.length; i++) { const line = lines[i] if (!line) { continue } const trimmedLine = line.trim() if (!trimmedLine) { continue } // Match markdown checkbox patterns const checkboxMatch = trimmedLine.match(/^\[([x\-\s])\]\s*(.+)$/i) if (checkboxMatch) { const statusChar = checkboxMatch[1] ?? " " const content = checkboxMatch[2] ?? "" let status: TodoItem["status"] = "pending" if (statusChar.toLowerCase() === "x") { status = "completed" } else if (statusChar === "-") { status = "in_progress" } todos.push({ id: `todo-${i}`, content: content.trim(), status }) } } return todos }