Roo-Code/apps/cli/src/ui/utils/tools.ts

323 lines
9.4 KiB
TypeScript

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<string, unknown>): 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<Record<string, unknown>>).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<Record<string, unknown>>).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, unknown>): 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, unknown>): 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<string, unknown>): 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<string, unknown>
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
}