From 8add1a66205d496868ae28bea581ee6b807388d2 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 3 Jan 2026 15:23:49 +0700 Subject: [PATCH] feat(tool-call-sheet): improve MCP tool call display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract key-value pairs from tool arguments and results - Add Input/Output group headers with visual separation - Fix Playwright MCP tool calls not being clickable (pass data.input as args) - Increase default sheet height to 60% - Add extractKeyValuePairs utility function using Zod 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../app/src/components/agent-stream-view.tsx | 13 +- .../app/src/components/tool-call-sheet.tsx | 156 ++++++++++++++---- packages/app/src/utils/tool-call-parsers.ts | 48 ++++++ 3 files changed, 178 insertions(+), 39 deletions(-) diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index a74f31f02..0d5aff7f9 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -290,7 +290,7 @@ export function AgentStreamView({ ({ opacity: 0.3 + dotOne.value * 0.7, transform: [{ translateY: dotOne.value * translateDistance }], @@ -612,7 +612,6 @@ function WorkingIndicator() { return ( - Working @@ -1066,6 +1065,9 @@ const stylesheet = StyleSheet.create((theme) => ({ }, workingIndicatorWrapper: { alignItems: "flex-start", + paddingLeft: 3, + paddingTop: theme.spacing[3], + paddingBottom: theme.spacing[2], }, workingIndicatorBubble: { flexDirection: "row", @@ -1079,11 +1081,6 @@ const stylesheet = StyleSheet.create((theme) => ({ borderWidth: 0, alignSelf: "flex-start", }, - workingIndicatorText: { - color: theme.colors.mutedForeground, - fontSize: theme.fontSize.sm, - fontWeight: theme.fontWeight.semibold, - }, workingDotsRow: { flexDirection: "row", alignItems: "center", diff --git a/packages/app/src/components/tool-call-sheet.tsx b/packages/app/src/components/tool-call-sheet.tsx index 56d8c5fb0..3fdbbdeb6 100644 --- a/packages/app/src/components/tool-call-sheet.tsx +++ b/packages/app/src/components/tool-call-sheet.tsx @@ -15,7 +15,14 @@ import { } from "@gorhom/bottom-sheet"; import { Pencil, Eye, SquareTerminal, Search, Wrench, X } from "lucide-react-native"; import { DiffViewer } from "./diff-viewer"; -import type { EditEntry, ReadEntry, CommandDetails, DiffLine } from "@/utils/tool-call-parsers"; +import { + extractKeyValuePairs, + type EditEntry, + type ReadEntry, + type CommandDetails, + type DiffLine, + type KeyValuePair, +} from "@/utils/tool-call-parsers"; // ----- Types ----- @@ -157,7 +164,7 @@ export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps) const bottomSheetRef = useRef(null); const [sheetData, setSheetData] = React.useState(null); - const snapPoints = useMemo(() => ["50%", "90%"], []); + const snapPoints = useMemo(() => ["60%", "90%"], []); const openToolCall = useCallback((data: ToolCallSheetData) => { setSheetData(data); @@ -321,31 +328,73 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) { // Render content sections const renderSections = useCallback(() => { const sections: ReactNode[] = []; + let hasOutput = false; // Always show args first if available if (args !== undefined) { + // Add Input group header sections.push( - - Arguments - - {serializedArgs} - + + Input ); + + const argPairs = extractKeyValuePairs(args); + if (argPairs.length > 0) { + argPairs.forEach((pair, index) => { + sections.push( + + {pair.key} + + {pair.value} + + + ); + }); + } else { + // Fallback to raw JSON display + sections.push( + + Arguments + + {serializedArgs} + + + ); + } } + // Helper to add output header once + const addOutputHeader = () => { + if (!hasOutput) { + hasOutput = true; + sections.push( + + Output + + ); + } + }; + // Render based on structured result type or raw data if (structuredResult) { switch (structuredResult.type) { case "command": { const cmd = parsedCommandDetails ?? extractCommandFromStructured(structuredResult); if (cmd) { + addOutputHeader(); sections.push( Command @@ -395,6 +444,9 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) { const diffs = parsedEditEntries?.length ? parsedEditEntries : extractDiffFromStructured(structuredResult); + if (diffs.length > 0) { + addOutputHeader(); + } diffs.forEach((entry, index) => { sections.push( @@ -417,6 +469,9 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) { const reads = parsedReadEntries?.length ? parsedReadEntries : extractReadFromStructured(structuredResult); + if (reads.length > 0) { + addOutputHeader(); + } reads.forEach((entry, index) => { sections.push( @@ -444,6 +499,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) { default: { if (result !== undefined && sections.length === 1) { // Only args shown, add result + addOutputHeader(); sections.push( Result @@ -463,28 +519,50 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) { } } } else if (result !== undefined) { - // No structured result - show raw result - sections.push( - - Result - - {serializedResult} - - - ); + // No structured result - try to extract key-value pairs + addOutputHeader(); + const keyValuePairs = extractKeyValuePairs(result); + if (keyValuePairs.length > 0) { + keyValuePairs.forEach((pair, index) => { + sections.push( + + {pair.key} + + {pair.value} + + + ); + }); + } else { + // Fallback to raw JSON display + sections.push( + + Result + + {serializedResult} + + + ); + } } // Always show errors if available if (error !== undefined) { sections.push( - Error + Error ({ }, contentContainer: { paddingHorizontal: theme.spacing[4], - paddingVertical: theme.spacing[4], - gap: theme.spacing[4], + paddingTop: theme.spacing[4], + paddingBottom: theme.spacing[8], + gap: theme.spacing[6], + }, + groupHeader: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + paddingBottom: theme.spacing[1], + borderBottomWidth: theme.borderWidth[1], + borderBottomColor: theme.colors.border, + }, + groupHeaderText: { + color: theme.colors.primary, + fontSize: theme.fontSize.sm, + fontWeight: theme.fontWeight.bold, + textTransform: "uppercase", + letterSpacing: 1, }, section: { gap: theme.spacing[2], diff --git a/packages/app/src/utils/tool-call-parsers.ts b/packages/app/src/utils/tool-call-parsers.ts index 522f33d95..b27dc98d2 100644 --- a/packages/app/src/utils/tool-call-parsers.ts +++ b/packages/app/src/utils/tool-call-parsers.ts @@ -704,3 +704,51 @@ export function extractCommandDetails(...sources: unknown[]): CommandDetails | n } return null; } + +// ---- Key-Value Extraction for Generic Tool Results ---- + +export interface KeyValuePair { + key: string; + value: string; +} + +const WrappedOutputSchema = z + .object({ output: z.record(z.unknown()) }) + .transform((data) => data.output); + +const DirectRecordSchema = z.record(z.unknown()); + +const ToolResultRecordSchema = z.union([WrappedOutputSchema, DirectRecordSchema]); + +function stringifyValue(value: unknown): string { + if (value === null) { + return "null"; + } + if (value === undefined) { + return "undefined"; + } + if (typeof value === "string") { + return value; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +export function extractKeyValuePairs(result: unknown): KeyValuePair[] { + const parsed = ToolResultRecordSchema.safeParse(result); + if (!parsed.success) { + return []; + } + + const record = parsed.data; + return Object.entries(record).map(([key, value]) => ({ + key, + value: stringifyValue(value), + })); +}