forked from bots-garden/ori
| ✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS | 1 | /** |
| 2 | * ToolCallCard shows one tool call the agent is running, in the spirit of | |
| 3 | * Zed's agent panel: a collapsible card with the tool kind, a live status, | |
| 4 | * touched locations, and the call's output (text, or a file diff). | |
| 5 | * | |
| 6 | * @example | |
| 7 | * <ToolCallCard call={{ toolCallId: "c1", title: "Reading main.go", | |
| 8 | * toolKind: "read", status: "in_progress", contents: [], locations: [] }} /> | |
| 9 | */ | |
| 10 | ||
| 11 | import type { ToolCall } from "../reducer"; | |
| 12 | import type { AcpToolCallContent } from "../protocol"; | |
| 13 | import { Markdown } from "./Markdown"; | |
| 14 | ||
| 15 | const kindIcons: Record<string, string> = { | |
| 16 | read: "📖", | |
| 17 | edit: "✏️", | |
| 18 | delete: "🗑️", | |
| 19 | move: "📦", | |
| 20 | search: "🔍", | |
| 21 | execute: "⚡", | |
| 22 | think: "💭", | |
| 23 | fetch: "🌐", | |
| 24 | switch_mode: "🔀", | |
| 25 | other: "🔧", | |
| 26 | }; | |
| 27 | ||
| 28 | const statusLabels: Record<string, string> = { | |
| 29 | pending: "pending", | |
| 30 | in_progress: "running", | |
| 31 | completed: "done", | |
| 32 | failed: "failed", | |
| 33 | }; | |
| 34 | ||
| 35 | export function ToolCallCard({ call }: { call: ToolCall }) { | |
| 36 | return ( | |
| 37 | <details className={`tool tool-${call.status}`}> | |
| 38 | <summary className="tool-summary"> | |
| 39 | <span className="tool-icon" aria-hidden> | |
| 40 | {kindIcons[call.toolKind] ?? kindIcons.other} | |
| 41 | </span> | |
| 42 | <span className="tool-title">{call.title}</span> | |
| 43 | <span className={`tool-status tool-status-${call.status}`}> | |
| 44 | {statusLabels[call.status] ?? call.status} | |
| 45 | </span> | |
| 46 | </summary> | |
| 47 | <div className="tool-body"> | |
| 48 | {call.locations.length > 0 && ( | |
| 49 | <ul className="tool-locations"> | |
| 50 | {call.locations.map((location, index) => ( | |
| 51 | // biome-ignore lint/suspicious/noArrayIndexKey: tool-call locations carry no id and the list is replaced wholesale on every update. | |
| 52 | <li key={index}> | |
| 53 | <code> | |
| 54 | {location.path} | |
| 55 | {location.line != null ? `:${location.line}` : ""} | |
| 56 | </code> | |
| 57 | </li> | |
| 58 | ))} | |
| 59 | </ul> | |
| 60 | )} | |
| 61 | {call.contents.map((content, index) => ( | |
| 62 | // biome-ignore lint/suspicious/noArrayIndexKey: tool-call contents carry no id and the list is replaced wholesale on every update. | |
| 63 | <ToolContent key={index} content={content} /> | |
| 64 | ))} | |
| 65 | </div> | |
| 66 | </details> | |
| 67 | ); | |
| 68 | } | |
| 69 | ||
| 70 | function ToolContent({ content }: { content: AcpToolCallContent }) { | |
| 71 | switch (content.type) { | |
| 72 | case "content": | |
| 73 | return content.content?.text ? ( | |
| 74 | <Markdown text={content.content.text} /> | |
| 75 | ) : null; | |
| 76 | case "diff": | |
| 77 | return ( | |
| 78 | <div className="tool-diff"> | |
| 79 | <code className="tool-diff-path">{content.path}</code> | |
| 80 | {content.oldText != null && ( | |
| 81 | <pre className="tool-diff-old"> | |
| 82 | {prefixLines(content.oldText, "- ")} | |
| 83 | </pre> | |
| 84 | )} | |
| 85 | <pre className="tool-diff-new"> | |
| 86 | {prefixLines(content.newText ?? "", "+ ")} | |
| 87 | </pre> | |
| 88 | </div> | |
| 89 | ); | |
| 90 | case "terminal": | |
| 91 | return <p className="tool-terminal">terminal: {content.terminalId}</p>; | |
| 92 | default: | |
| 93 | return null; | |
| 94 | } | |
| 95 | } | |
| 96 | ||
| 97 | function prefixLines(text: string, prefix: string): string { | |
| 98 | return text | |
| 99 | .split("\n") | |
| 100 | .map((line) => prefix + line) | |
| 101 | .join("\n"); | |
| 102 | } |