From 2347b0e0bbbc7c7e62334cef1eabc1e6d86bda22 Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Fri, 27 Feb 2026 13:22:59 +0100 Subject: [PATCH] wip --- .../components/session-update/McpToolView.tsx | 35 ++---------- .../session-update/ToolCallBlock.tsx | 2 +- .../session-update/ToolCallView.tsx | 50 ++++++++++++----- .../session-update/toolCallUtils.tsx | 29 ++++++++++ .../claude/conversion/tool-use-to-acp.ts | 54 +++++++++++++------ 5 files changed, 111 insertions(+), 59 deletions(-) diff --git a/apps/twig/src/renderer/features/sessions/components/session-update/McpToolView.tsx b/apps/twig/src/renderer/features/sessions/components/session-update/McpToolView.tsx index 03b4fbd4e..3b7d514c7 100644 --- a/apps/twig/src/renderer/features/sessions/components/session-update/McpToolView.tsx +++ b/apps/twig/src/renderer/features/sessions/components/session-update/McpToolView.tsx @@ -2,52 +2,29 @@ import { Plugs } from "@phosphor-icons/react"; import { Box, Flex } from "@radix-ui/themes"; import { useState } from "react"; import { + compactInput, ExpandableIcon, ExpandedContentBox, + formatInput, getContentText, StatusIndicators, + stripCodeFences, ToolTitle, type ToolViewProps, useToolCallStatus, } from "./toolCallUtils"; -const INPUT_PREVIEW_MAX_LENGTH = 60; - function parseMcpName(mcpToolName: string): { serverName: string; toolName: string; } { const parts = mcpToolName.split("__"); - const serverName = parts[1] ?? ""; return { - serverName: serverName.toLowerCase() === "posthog" ? "PostHog" : serverName, + serverName: parts[1] ?? "", toolName: parts.slice(2).join("__"), }; } -function compactInput(rawInput: unknown): string | undefined { - if (!rawInput || typeof rawInput !== "object") return undefined; - try { - const json = JSON.stringify(rawInput); - if (json === "{}") return undefined; - if (json.length <= INPUT_PREVIEW_MAX_LENGTH) return json; - return `${json.slice(0, INPUT_PREVIEW_MAX_LENGTH)}...`; - } catch { - return undefined; - } -} - -function formatInput(rawInput: unknown): string | undefined { - if (!rawInput || typeof rawInput !== "object") return undefined; - try { - const json = JSON.stringify(rawInput, null, 2); - if (json === "{}") return undefined; - return json; - } catch { - return undefined; - } -} - interface McpToolViewProps extends ToolViewProps { mcpToolName: string; } @@ -122,7 +99,3 @@ export function McpToolView({ ); } - -function stripCodeFences(text: string): string { - return text.replace(/^```\w*\n?/, "").replace(/\n?```\s*$/, ""); -} diff --git a/apps/twig/src/renderer/features/sessions/components/session-update/ToolCallBlock.tsx b/apps/twig/src/renderer/features/sessions/components/session-update/ToolCallBlock.tsx index 42924fe2e..a17183527 100644 --- a/apps/twig/src/renderer/features/sessions/components/session-update/ToolCallBlock.tsx +++ b/apps/twig/src/renderer/features/sessions/components/session-update/ToolCallBlock.tsx @@ -91,7 +91,7 @@ export function ToolCallBlock({ case "question": return ; default: - return ; + return ; } })(); diff --git a/apps/twig/src/renderer/features/sessions/components/session-update/ToolCallView.tsx b/apps/twig/src/renderer/features/sessions/components/session-update/ToolCallView.tsx index bb0c040f7..4e00bd587 100644 --- a/apps/twig/src/renderer/features/sessions/components/session-update/ToolCallView.tsx +++ b/apps/twig/src/renderer/features/sessions/components/session-update/ToolCallView.tsx @@ -4,6 +4,7 @@ import { ArrowsLeftRight, Brain, ChatCircle, + Command, FileText, Globe, type Icon, @@ -17,11 +18,14 @@ import { Box, Flex } from "@radix-ui/themes"; import { compactHomePath } from "@utils/path"; import { useState } from "react"; import { + compactInput, ExpandableIcon, ExpandedContentBox, + formatInput, getContentText, getFilename, StatusIndicators, + stripCodeFences, ToolTitle, type ToolViewProps, useToolCallStatus, @@ -41,20 +45,33 @@ const kindIcons: Record = { other: Wrench, }; +const toolNameIcons: Record = { + ToolSearch: MagnifyingGlass, + Skill: Command, +}; + +interface ToolCallViewProps extends ToolViewProps { + agentToolName?: string; +} + export function ToolCallView({ toolCall, turnCancelled, turnComplete, + agentToolName, expanded = false, -}: ToolViewProps) { +}: ToolCallViewProps) { const [isExpanded, setIsExpanded] = useState(expanded); - const { title, kind, status, locations, content } = toolCall; - const { isLoading, isFailed, wasCancelled } = useToolCallStatus( + const { title, kind, status, locations, content, rawInput } = toolCall; + const { isLoading, isFailed, wasCancelled, isComplete } = useToolCallStatus( status, turnCancelled, turnComplete, ); - const KindIcon = (kind && kindIcons[kind]) || Wrench; + const KindIcon = + (agentToolName && toolNameIcons[agentToolName]) || + (kind && kindIcons[kind]) || + Wrench; const filePath = kind === "read" && locations?.[0]?.path; const displayText = filePath @@ -63,9 +80,12 @@ export function ToolCallView({ ? compactHomePath(title) : undefined; + const inputPreview = compactInput(rawInput); + const fullInput = formatInput(rawInput); + const output = stripCodeFences(getContentText(content) ?? ""); const hasOutput = output.trim().length > 0; - const isExpandable = hasOutput; + const isExpandable = !!fullInput || hasOutput; const handleClick = () => { if (isExpandable) { @@ -87,19 +107,25 @@ export function ToolCallView({ isExpanded={isExpanded} /> - + {displayText} + {inputPreview && ( + + {inputPreview} + + )} - {isExpanded && hasOutput && ( - {output} + {isExpanded && ( + <> + {fullInput && {fullInput}} + {isComplete && hasOutput && ( + {output} + )} + )} ); } - -function stripCodeFences(text: string): string { - return text.replace(/^```\w*\n?/, "").replace(/\n?```\s*$/, ""); -} diff --git a/apps/twig/src/renderer/features/sessions/components/session-update/toolCallUtils.tsx b/apps/twig/src/renderer/features/sessions/components/session-update/toolCallUtils.tsx index c28f0d3de..46ee21656 100644 --- a/apps/twig/src/renderer/features/sessions/components/session-update/toolCallUtils.tsx +++ b/apps/twig/src/renderer/features/sessions/components/session-update/toolCallUtils.tsx @@ -89,6 +89,35 @@ export function getLineCount(content: ToolCall["content"]): number | null { return text ? text.split("\n").length : null; } +const INPUT_PREVIEW_MAX_LENGTH = 60; + +export function compactInput(rawInput: unknown): string | undefined { + if (!rawInput || typeof rawInput !== "object") return undefined; + try { + const json = JSON.stringify(rawInput); + if (json === "{}") return undefined; + if (json.length <= INPUT_PREVIEW_MAX_LENGTH) return json; + return `${json.slice(0, INPUT_PREVIEW_MAX_LENGTH)}...`; + } catch { + return undefined; + } +} + +export function formatInput(rawInput: unknown): string | undefined { + if (!rawInput || typeof rawInput !== "object") return undefined; + try { + const json = JSON.stringify(rawInput, null, 2); + if (json === "{}") return undefined; + return json; + } catch { + return undefined; + } +} + +export function stripCodeFences(text: string): string { + return text.replace(/^```\w*\n?/, "").replace(/\n?```\s*$/, ""); +} + export function truncateText( text: string, maxLength: number, diff --git a/packages/agent/src/adapters/claude/conversion/tool-use-to-acp.ts b/packages/agent/src/adapters/claude/conversion/tool-use-to-acp.ts index b5f73b74c..a895ae94b 100644 --- a/packages/agent/src/adapters/claude/conversion/tool-use-to-acp.ts +++ b/packages/agent/src/adapters/claude/conversion/tool-use-to-acp.ts @@ -609,32 +609,56 @@ export function toolUpdateFromToolResult( } } +function itemToText(item: unknown): string | null { + if (!item || typeof item !== "object") return null; + const obj = item as Record; + // Standard text block + if (obj.type === "text" && typeof obj.text === "string") { + return obj.text; + } + // Any other structured object — serialize it + try { + return JSON.stringify(obj, null, 2); + } catch { + return null; + } +} + function toAcpContentUpdate( content: unknown, isError: boolean = false, ): Pick { if (Array.isArray(content) && content.length > 0) { - return { - content: content.map((item) => { - const itemObj = item as { type?: string; text?: string }; - if (isError && itemObj.type === "text") { - return { - type: "content" as const, - content: text(`\`\`\`\n${itemObj.text ?? ""}\n\`\`\``), - }; - } - return { - type: "content" as const, - content: item as { type: "text"; text: string }, - }; - }), - }; + const texts: string[] = []; + for (const item of content) { + const t = itemToText(item); + if (t) texts.push(t); + } + if (texts.length > 0) { + const combined = texts.join("\n"); + return { + content: toolContent() + .text(isError ? `\`\`\`\n${combined}\n\`\`\`` : combined) + .build(), + }; + } } else if (typeof content === "string" && content.length > 0) { return { content: toolContent() .text(isError ? `\`\`\`\n${content}\n\`\`\`` : content) .build(), }; + } else if (content && typeof content === "object") { + try { + const json = JSON.stringify(content, null, 2); + if (json && json !== "{}") { + return { + content: toolContent().text(json).build(), + }; + } + } catch { + // ignore serialization errors + } } return {}; }