Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -122,7 +99,3 @@ export function McpToolView({
</Box>
);
}

function stripCodeFences(text: string): string {
return text.replace(/^```\w*\n?/, "").replace(/\n?```\s*$/, "");
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function ToolCallBlock({
case "question":
return <QuestionToolView {...props} />;
default:
return <ToolCallView {...props} />;
return <ToolCallView {...props} agentToolName={toolName} />;
}
})();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ArrowsLeftRight,
Brain,
ChatCircle,
Command,
FileText,
Globe,
type Icon,
Expand All @@ -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,
Expand All @@ -41,20 +45,33 @@ const kindIcons: Record<TwigToolKind, Icon> = {
other: Wrench,
};

const toolNameIcons: Record<string, Icon> = {
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
Expand All @@ -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) {
Expand All @@ -87,19 +107,25 @@ export function ToolCallView({
isExpanded={isExpanded}
/>
</Box>
<Flex align="center" gap="2" wrap="wrap">
<Flex align="center" gap="1" wrap="wrap" className="min-w-0">
<ToolTitle>{displayText}</ToolTitle>
{inputPreview && (
<ToolTitle>
<span className="font-mono text-accent-11">{inputPreview}</span>
</ToolTitle>
)}
<StatusIndicators isFailed={isFailed} wasCancelled={wasCancelled} />
</Flex>
</Flex>

{isExpanded && hasOutput && (
<ExpandedContentBox>{output}</ExpandedContentBox>
{isExpanded && (
<>
{fullInput && <ExpandedContentBox>{fullInput}</ExpandedContentBox>}
{isComplete && hasOutput && (
<ExpandedContentBox>{output}</ExpandedContentBox>
)}
</>
)}
</Box>
);
}

function stripCodeFences(text: string): string {
return text.replace(/^```\w*\n?/, "").replace(/\n?```\s*$/, "");
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
54 changes: 39 additions & 15 deletions packages/agent/src/adapters/claude/conversion/tool-use-to-acp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
// 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<ToolCallUpdate, "content"> {
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 {};
}
Expand Down
Loading