tools.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import type { TodoItem } from "@roo-code/types"
  2. import type { ToolData } from "../types.js"
  3. /**
  4. * Extract structured ToolData from parsed tool JSON
  5. * This provides rich data for tool-specific renderers
  6. */
  7. export function extractToolData(toolInfo: Record<string, unknown>): ToolData {
  8. const toolName = (toolInfo.tool as string) || "unknown"
  9. // Base tool data with common fields
  10. const toolData: ToolData = {
  11. tool: toolName,
  12. path: toolInfo.path as string | undefined,
  13. isOutsideWorkspace: toolInfo.isOutsideWorkspace as boolean | undefined,
  14. isProtected: toolInfo.isProtected as boolean | undefined,
  15. content: toolInfo.content as string | undefined,
  16. reason: toolInfo.reason as string | undefined,
  17. }
  18. // Extract diff-related fields
  19. if (toolInfo.diff !== undefined) {
  20. toolData.diff = toolInfo.diff as string
  21. }
  22. if (toolInfo.diffStats !== undefined) {
  23. const stats = toolInfo.diffStats as { added?: number; removed?: number }
  24. if (typeof stats.added === "number" && typeof stats.removed === "number") {
  25. toolData.diffStats = { added: stats.added, removed: stats.removed }
  26. }
  27. }
  28. // Extract search-related fields
  29. if (toolInfo.regex !== undefined) {
  30. toolData.regex = toolInfo.regex as string
  31. }
  32. if (toolInfo.filePattern !== undefined) {
  33. toolData.filePattern = toolInfo.filePattern as string
  34. }
  35. if (toolInfo.query !== undefined) {
  36. toolData.query = toolInfo.query as string
  37. }
  38. // Extract mode-related fields
  39. if (toolInfo.mode !== undefined) {
  40. toolData.mode = toolInfo.mode as string
  41. }
  42. if (toolInfo.mode_slug !== undefined) {
  43. toolData.mode = toolInfo.mode_slug as string
  44. }
  45. // Extract command-related fields
  46. if (toolInfo.command !== undefined) {
  47. toolData.command = toolInfo.command as string
  48. }
  49. if (toolInfo.output !== undefined) {
  50. toolData.output = toolInfo.output as string
  51. }
  52. // Extract batch file operations
  53. if (Array.isArray(toolInfo.files)) {
  54. toolData.batchFiles = (toolInfo.files as Array<Record<string, unknown>>).map((f) => ({
  55. path: (f.path as string) || "",
  56. lineSnippet: f.lineSnippet as string | undefined,
  57. isOutsideWorkspace: f.isOutsideWorkspace as boolean | undefined,
  58. key: f.key as string | undefined,
  59. content: f.content as string | undefined,
  60. }))
  61. }
  62. // Extract batch diff operations
  63. if (Array.isArray(toolInfo.batchDiffs)) {
  64. toolData.batchDiffs = (toolInfo.batchDiffs as Array<Record<string, unknown>>).map((d) => ({
  65. path: (d.path as string) || "",
  66. changeCount: d.changeCount as number | undefined,
  67. key: d.key as string | undefined,
  68. content: d.content as string | undefined,
  69. diffStats: d.diffStats as { added: number; removed: number } | undefined,
  70. diffs: d.diffs as Array<{ content: string; startLine?: number }> | undefined,
  71. }))
  72. }
  73. // Extract question/completion fields
  74. if (toolInfo.question !== undefined) {
  75. toolData.question = toolInfo.question as string
  76. }
  77. if (toolInfo.result !== undefined) {
  78. toolData.result = toolInfo.result as string
  79. }
  80. // Extract additional display hints
  81. if (toolInfo.lineNumber !== undefined) {
  82. toolData.lineNumber = toolInfo.lineNumber as number
  83. }
  84. if (toolInfo.additionalFileCount !== undefined) {
  85. toolData.additionalFileCount = toolInfo.additionalFileCount as number
  86. }
  87. return toolData
  88. }
  89. /**
  90. * Format tool output for display (used in the message body, header shows tool name separately)
  91. */
  92. export function formatToolOutput(toolInfo: Record<string, unknown>): string {
  93. const toolName = (toolInfo.tool as string) || "unknown"
  94. switch (toolName) {
  95. case "switchMode": {
  96. const mode = (toolInfo.mode as string) || "unknown"
  97. const reason = toolInfo.reason as string
  98. return `→ ${mode} mode${reason ? `\n ${reason}` : ""}`
  99. }
  100. case "switch_mode": {
  101. const mode = (toolInfo.mode_slug as string) || (toolInfo.mode as string) || "unknown"
  102. const reason = toolInfo.reason as string
  103. return `→ ${mode} mode${reason ? `\n ${reason}` : ""}`
  104. }
  105. case "execute_command": {
  106. const command = toolInfo.command as string
  107. return `$ ${command || "(no command)"}`
  108. }
  109. case "read_file": {
  110. const files = toolInfo.files as Array<{ path: string }> | undefined
  111. const path = toolInfo.path as string
  112. if (files && files.length > 0) {
  113. return files.map((f) => `📄 ${f.path}`).join("\n")
  114. }
  115. return `📄 ${path || "(no path)"}`
  116. }
  117. case "write_to_file": {
  118. const writePath = toolInfo.path as string
  119. return `📝 ${writePath || "(no path)"}`
  120. }
  121. case "apply_diff": {
  122. const diffPath = toolInfo.path as string
  123. return `✏️ ${diffPath || "(no path)"}`
  124. }
  125. case "search_files": {
  126. const searchPath = toolInfo.path as string
  127. const regex = toolInfo.regex as string
  128. return `🔍 "${regex}" in ${searchPath || "."}`
  129. }
  130. case "list_files": {
  131. const listPath = toolInfo.path as string
  132. const recursive = toolInfo.recursive as boolean
  133. return `📁 ${listPath || "."}${recursive ? " (recursive)" : ""}`
  134. }
  135. case "attempt_completion": {
  136. const result = toolInfo.result as string
  137. if (result) {
  138. const truncated = result.length > 100 ? result.substring(0, 100) + "..." : result
  139. return `✅ ${truncated}`
  140. }
  141. return "✅ Task completed"
  142. }
  143. case "ask_followup_question": {
  144. const question = toolInfo.question as string
  145. return `❓ ${question || "(no question)"}`
  146. }
  147. case "new_task": {
  148. const taskMode = toolInfo.mode as string
  149. return `📋 Creating subtask${taskMode ? ` in ${taskMode} mode` : ""}`
  150. }
  151. case "update_todo_list":
  152. case "updateTodoList": {
  153. // Special marker - actual rendering is handled by TodoChangeDisplay component
  154. return "☑ TODO list updated"
  155. }
  156. default: {
  157. const params = Object.entries(toolInfo)
  158. .filter(([key]) => key !== "tool")
  159. .map(([key, value]) => {
  160. const displayValue = typeof value === "string" ? value : JSON.stringify(value)
  161. const truncated = displayValue.length > 100 ? displayValue.substring(0, 100) + "..." : displayValue
  162. return `${key}: ${truncated}`
  163. })
  164. .join("\n")
  165. return params || "(no parameters)"
  166. }
  167. }
  168. }
  169. /**
  170. * Format tool ask message for user approval prompt
  171. */
  172. export function formatToolAskMessage(toolInfo: Record<string, unknown>): string {
  173. const toolName = (toolInfo.tool as string) || "unknown"
  174. switch (toolName) {
  175. case "switchMode":
  176. case "switch_mode": {
  177. const mode = (toolInfo.mode as string) || (toolInfo.mode_slug as string) || "unknown"
  178. const reason = toolInfo.reason as string
  179. return `Switch to ${mode} mode?${reason ? `\nReason: ${reason}` : ""}`
  180. }
  181. case "execute_command": {
  182. const command = toolInfo.command as string
  183. return `Run command?\n$ ${command || "(no command)"}`
  184. }
  185. case "read_file": {
  186. const files = toolInfo.files as Array<{ path: string }> | undefined
  187. const path = toolInfo.path as string
  188. if (files && files.length > 0) {
  189. return `Read ${files.length} file(s)?\n${files.map((f) => ` ${f.path}`).join("\n")}`
  190. }
  191. return `Read file: ${path || "(no path)"}`
  192. }
  193. case "write_to_file": {
  194. const writePath = toolInfo.path as string
  195. return `Write to file: ${writePath || "(no path)"}`
  196. }
  197. case "apply_diff": {
  198. const diffPath = toolInfo.path as string
  199. return `Apply changes to: ${diffPath || "(no path)"}`
  200. }
  201. default: {
  202. const params = Object.entries(toolInfo)
  203. .filter(([key]) => key !== "tool")
  204. .map(([key, value]) => {
  205. const displayValue = typeof value === "string" ? value : JSON.stringify(value)
  206. const truncated = displayValue.length > 80 ? displayValue.substring(0, 80) + "..." : displayValue
  207. return ` ${key}: ${truncated}`
  208. })
  209. .join("\n")
  210. return `${toolName}${params ? `\n${params}` : ""}`
  211. }
  212. }
  213. }
  214. /**
  215. * Parse TODO items from tool info
  216. * Handles both array format and markdown checklist string format
  217. */
  218. export function parseTodosFromToolInfo(toolInfo: Record<string, unknown>): TodoItem[] | null {
  219. // Try to get todos directly as an array
  220. const todosArray = toolInfo.todos as unknown[] | undefined
  221. if (Array.isArray(todosArray)) {
  222. return todosArray
  223. .map((item, index) => {
  224. if (typeof item === "object" && item !== null) {
  225. const todo = item as Record<string, unknown>
  226. return {
  227. id: (todo.id as string) || `todo-${index}`,
  228. content: (todo.content as string) || "",
  229. status: ((todo.status as string) || "pending") as TodoItem["status"],
  230. }
  231. }
  232. return null
  233. })
  234. .filter((item): item is TodoItem => item !== null)
  235. }
  236. // Try to parse markdown checklist format from todos string
  237. const todosString = toolInfo.todos as string | undefined
  238. if (typeof todosString === "string") {
  239. return parseMarkdownChecklist(todosString)
  240. }
  241. return null
  242. }
  243. /**
  244. * Parse a markdown checklist string into TodoItem array
  245. * Format:
  246. * [ ] pending item
  247. * [-] in progress item
  248. * [x] completed item
  249. */
  250. export function parseMarkdownChecklist(markdown: string): TodoItem[] {
  251. const lines = markdown.split("\n")
  252. const todos: TodoItem[] = []
  253. for (let i = 0; i < lines.length; i++) {
  254. const line = lines[i]
  255. if (!line) {
  256. continue
  257. }
  258. const trimmedLine = line.trim()
  259. if (!trimmedLine) {
  260. continue
  261. }
  262. // Match markdown checkbox patterns
  263. const checkboxMatch = trimmedLine.match(/^\[([x\-\s])\]\s*(.+)$/i)
  264. if (checkboxMatch) {
  265. const statusChar = checkboxMatch[1] ?? " "
  266. const content = checkboxMatch[2] ?? ""
  267. let status: TodoItem["status"] = "pending"
  268. if (statusChar.toLowerCase() === "x") {
  269. status = "completed"
  270. } else if (statusChar === "-") {
  271. status = "in_progress"
  272. }
  273. todos.push({ id: `todo-${i}`, content: content.trim(), status })
  274. }
  275. }
  276. return todos
  277. }