mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-28 01:31:35 +00:00
323 lines
9.4 KiB
TypeScript
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
|
|
}
|