feat(tool-call-sheet): improve MCP tool call display

- 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 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2026-01-03 15:23:49 +07:00
co-authored by Claude Opus 4.5
parent 914cebc914
commit 8add1a6620
3 changed files with 178 additions and 39 deletions
@@ -290,7 +290,7 @@ export function AgentStreamView({
<ToolCall
toolName={toolLabel}
kind={data.kind}
args={undefined}
args={data.input}
result={data.result}
error={data.error}
status={data.status as "executing" | "completed" | "failed"}
@@ -594,7 +594,7 @@ function WorkingIndicator() {
};
}, [dotOne, dotTwo, dotThree]);
const translateDistance = -4;
const translateDistance = -2;
const dotOneStyle = useAnimatedStyle(() => ({
opacity: 0.3 + dotOne.value * 0.7,
transform: [{ translateY: dotOne.value * translateDistance }],
@@ -612,7 +612,6 @@ function WorkingIndicator() {
return (
<View style={stylesheet.workingIndicatorBubble}>
<Text style={stylesheet.workingIndicatorText}>Working</Text>
<View style={stylesheet.workingDotsRow}>
<Animated.View style={[stylesheet.workingDot, dotOneStyle]} />
<Animated.View style={[stylesheet.workingDot, dotTwoStyle]} />
@@ -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",
+125 -31
View File
@@ -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<BottomSheetModal>(null);
const [sheetData, setSheetData] = React.useState<ToolCallSheetData | null>(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(
<View key="args" style={styles.section}>
<Text style={styles.sectionTitle}>Arguments</Text>
<ScrollView
horizontal
nestedScrollEnabled
style={styles.jsonScroll}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>
<Text style={styles.scrollText}>{serializedArgs}</Text>
</ScrollView>
<View key="input-header" style={styles.groupHeader}>
<Text style={styles.groupHeaderText}>Input</Text>
</View>
);
const argPairs = extractKeyValuePairs(args);
if (argPairs.length > 0) {
argPairs.forEach((pair, index) => {
sections.push(
<View key={`arg-${index}-${pair.key}`} style={styles.section}>
<Text style={styles.sectionTitle}>{pair.key}</Text>
<ScrollView
horizontal
nestedScrollEnabled
style={styles.jsonScroll}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>
<Text style={styles.scrollText}>{pair.value}</Text>
</ScrollView>
</View>
);
});
} else {
// Fallback to raw JSON display
sections.push(
<View key="args" style={styles.section}>
<Text style={styles.sectionTitle}>Arguments</Text>
<ScrollView
horizontal
nestedScrollEnabled
style={styles.jsonScroll}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>
<Text style={styles.scrollText}>{serializedArgs}</Text>
</ScrollView>
</View>
);
}
}
// Helper to add output header once
const addOutputHeader = () => {
if (!hasOutput) {
hasOutput = true;
sections.push(
<View key="output-header" style={styles.groupHeader}>
<Text style={styles.groupHeaderText}>Output</Text>
</View>
);
}
};
// 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(
<View key="command" style={styles.section}>
<Text style={styles.sectionTitle}>Command</Text>
@@ -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(
<View key={`diff-${index}`} style={styles.section}>
@@ -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(
<View key={`read-${index}`} style={styles.section}>
@@ -444,6 +499,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
default: {
if (result !== undefined && sections.length === 1) {
// Only args shown, add result
addOutputHeader();
sections.push(
<View key="result" style={styles.section}>
<Text style={styles.sectionTitle}>Result</Text>
@@ -463,28 +519,50 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
}
}
} else if (result !== undefined) {
// No structured result - show raw result
sections.push(
<View key="result" style={styles.section}>
<Text style={styles.sectionTitle}>Result</Text>
<ScrollView
horizontal
nestedScrollEnabled
style={styles.jsonScroll}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>
<Text style={styles.scrollText}>{serializedResult}</Text>
</ScrollView>
</View>
);
// No structured result - try to extract key-value pairs
addOutputHeader();
const keyValuePairs = extractKeyValuePairs(result);
if (keyValuePairs.length > 0) {
keyValuePairs.forEach((pair, index) => {
sections.push(
<View key={`kv-${index}-${pair.key}`} style={styles.section}>
<Text style={styles.sectionTitle}>{pair.key}</Text>
<ScrollView
horizontal
nestedScrollEnabled
style={styles.jsonScroll}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>
<Text style={styles.scrollText}>{pair.value}</Text>
</ScrollView>
</View>
);
});
} else {
// Fallback to raw JSON display
sections.push(
<View key="result" style={styles.section}>
<Text style={styles.sectionTitle}>Result</Text>
<ScrollView
horizontal
nestedScrollEnabled
style={styles.jsonScroll}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>
<Text style={styles.scrollText}>{serializedResult}</Text>
</ScrollView>
</View>
);
}
}
// Always show errors if available
if (error !== undefined) {
sections.push(
<View key="error" style={styles.section}>
<Text style={styles.sectionTitle}>Error</Text>
<Text style={[styles.sectionTitle, styles.errorText]}>Error</Text>
<ScrollView
horizontal
nestedScrollEnabled
@@ -644,8 +722,24 @@ const styles = StyleSheet.create((theme) => ({
},
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],
@@ -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),
}));
}