feat(app): extract tool call details for inline/sheet display
- Create ToolCallDetailsContent component for shared content rendering - Add useToolCallDetails hook for parsing tool call data - Desktop (md+): expand tool call details inline below badge - Mobile (xs/sm): open bottom sheet on tap (unchanged behavior) - Remove unused props from ToolCall component 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -280,9 +280,6 @@ export function AgentStreamView({
|
|||||||
result={data.result}
|
result={data.result}
|
||||||
error={data.error}
|
error={data.error}
|
||||||
status={data.status as "executing" | "completed" | "failed"}
|
status={data.status as "executing" | "completed" | "failed"}
|
||||||
parsedEditEntries={data.parsedEdits}
|
|
||||||
parsedReadEntries={data.parsedReads}
|
|
||||||
parsedCommandDetails={data.parsedCommand ?? null}
|
|
||||||
cwd={agent.cwd}
|
cwd={agent.cwd}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,16 +22,16 @@ import {
|
|||||||
Search,
|
Search,
|
||||||
Brain,
|
Brain,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles, UnistylesRuntime } from "react-native-unistyles";
|
||||||
import { baseColors, theme } from "@/styles/theme";
|
import { baseColors, theme } from "@/styles/theme";
|
||||||
import { createMarkdownStyles, createCompactMarkdownStyles } from "@/styles/markdown-styles";
|
import { createMarkdownStyles, createCompactMarkdownStyles } from "@/styles/markdown-styles";
|
||||||
import { Colors, Fonts } from "@/constants/theme";
|
import { Colors, Fonts } from "@/constants/theme";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import type { TodoEntry, ThoughtStatus } from "@/types/stream";
|
import type { TodoEntry, ThoughtStatus } from "@/types/stream";
|
||||||
import type { CommandDetails, EditEntry, ReadEntry } from "@/utils/tool-call-parsers";
|
|
||||||
import { extractPrincipalParam } from "@/utils/tool-call-parsers";
|
import { extractPrincipalParam } from "@/utils/tool-call-parsers";
|
||||||
import { resolveToolCallPreview } from "./tool-call-preview";
|
import { resolveToolCallPreview } from "./tool-call-preview";
|
||||||
import { useToolCallSheet } from "./tool-call-sheet";
|
import { useToolCallSheet } from "./tool-call-sheet";
|
||||||
|
import { ToolCallDetailsContent, useToolCallDetails } from "./tool-call-details";
|
||||||
|
|
||||||
interface UserMessageProps {
|
interface UserMessageProps {
|
||||||
message: string;
|
message: string;
|
||||||
@@ -845,7 +845,7 @@ interface ExpandableBadgeProps {
|
|||||||
secondaryLabel?: string;
|
secondaryLabel?: string;
|
||||||
icon?: ComponentType<{ size?: number; color?: string }>;
|
icon?: ComponentType<{ size?: number; color?: string }>;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
onToggle: () => void;
|
onToggle?: () => void;
|
||||||
renderDetails?: () => ReactNode;
|
renderDetails?: () => ReactNode;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
@@ -1110,9 +1110,6 @@ interface ToolCallProps {
|
|||||||
result?: any;
|
result?: any;
|
||||||
error?: any;
|
error?: any;
|
||||||
status: "executing" | "completed" | "failed";
|
status: "executing" | "completed" | "failed";
|
||||||
parsedEditEntries?: EditEntry[];
|
|
||||||
parsedReadEntries?: ReadEntry[];
|
|
||||||
parsedCommandDetails?: CommandDetails | null;
|
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1141,12 +1138,14 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
result,
|
result,
|
||||||
error,
|
error,
|
||||||
status,
|
status,
|
||||||
parsedEditEntries,
|
|
||||||
parsedReadEntries,
|
|
||||||
parsedCommandDetails,
|
|
||||||
cwd,
|
cwd,
|
||||||
}: ToolCallProps) {
|
}: ToolCallProps) {
|
||||||
const { openToolCall } = useToolCallSheet();
|
const { openToolCall } = useToolCallSheet();
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
// Check if we're on mobile (use bottom sheet) or desktop (inline expand)
|
||||||
|
const isMobile =
|
||||||
|
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
|
|
||||||
const kind = getToolKindFromName(toolName);
|
const kind = getToolKindFromName(toolName);
|
||||||
const IconComponent = toolKindIcons[kind] || Wrench;
|
const IconComponent = toolKindIcons[kind] || Wrench;
|
||||||
@@ -1157,33 +1156,56 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
[args, cwd]
|
[args, cwd]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if there's any content to display in the sheet
|
// Check if there's any content to display
|
||||||
const hasDetails = args !== undefined || result !== undefined || error !== undefined;
|
const hasDetails = args !== undefined || result !== undefined || error !== undefined;
|
||||||
|
|
||||||
const handleOpenSheet = useCallback(() => {
|
// Parse tool call details for inline rendering
|
||||||
openToolCall({
|
const { display, errorText } = useToolCallDetails({ args, result, error });
|
||||||
toolName,
|
|
||||||
kind,
|
|
||||||
status,
|
|
||||||
args,
|
|
||||||
result,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
}, [openToolCall, toolName, kind, status, args, result, error]);
|
|
||||||
|
|
||||||
// Dummy renderDetails to make badge tappable - actual content is rendered in the sheet
|
const handleToggle = useCallback(() => {
|
||||||
const dummyRenderDetails = useCallback(() => null, []);
|
if (isMobile) {
|
||||||
|
// Mobile: open bottom sheet
|
||||||
|
openToolCall({
|
||||||
|
toolName,
|
||||||
|
kind,
|
||||||
|
status,
|
||||||
|
args,
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Desktop: toggle inline expansion
|
||||||
|
setIsExpanded((prev) => !prev);
|
||||||
|
}
|
||||||
|
}, [isMobile, openToolCall, toolName, kind, status, args, result, error]);
|
||||||
|
|
||||||
|
// Render inline details for desktop
|
||||||
|
const renderDetails = useCallback(() => {
|
||||||
|
if (isMobile) return null;
|
||||||
|
return (
|
||||||
|
<View style={toolCallInlineStyles.detailsContainer}>
|
||||||
|
<ToolCallDetailsContent display={display} errorText={errorText} maxHeight={400} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}, [isMobile, display, errorText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandableBadge
|
<ExpandableBadge
|
||||||
label={toolName}
|
label={toolName}
|
||||||
secondaryLabel={principalParam}
|
secondaryLabel={principalParam}
|
||||||
icon={IconComponent}
|
icon={IconComponent}
|
||||||
isExpanded={false}
|
isExpanded={!isMobile && isExpanded}
|
||||||
onToggle={handleOpenSheet}
|
onToggle={hasDetails ? handleToggle : undefined}
|
||||||
renderDetails={hasDetails ? dummyRenderDetails : undefined}
|
renderDetails={hasDetails && !isMobile ? renderDetails : (hasDetails ? () => null : undefined)}
|
||||||
isLoading={status === "executing"}
|
isLoading={status === "executing"}
|
||||||
isError={status === "failed"}
|
isError={status === "failed"}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toolCallInlineStyles = StyleSheet.create((theme) => ({
|
||||||
|
detailsContainer: {
|
||||||
|
paddingTop: theme.spacing[3],
|
||||||
|
paddingBottom: theme.spacing[2],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
import React, { useMemo, ReactNode } from "react";
|
||||||
|
import { View, Text } from "react-native";
|
||||||
|
import { ScrollView } from "react-native-gesture-handler";
|
||||||
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
|
import { Fonts } from "@/constants/theme";
|
||||||
|
import {
|
||||||
|
parseToolCallDisplay,
|
||||||
|
buildLineDiff,
|
||||||
|
type ToolCallDisplay,
|
||||||
|
} from "@/utils/tool-call-parsers";
|
||||||
|
import { DiffViewer } from "./diff-viewer";
|
||||||
|
|
||||||
|
// ---- Types ----
|
||||||
|
|
||||||
|
export interface ToolCallDetailsData {
|
||||||
|
args?: unknown;
|
||||||
|
result?: unknown;
|
||||||
|
error?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Helper ----
|
||||||
|
|
||||||
|
function formatValue(value: unknown): string {
|
||||||
|
if (value === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Content Component ----
|
||||||
|
|
||||||
|
interface ToolCallDetailsContentProps {
|
||||||
|
display: ToolCallDisplay;
|
||||||
|
errorText?: string;
|
||||||
|
maxHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToolCallDetailsContent({
|
||||||
|
display,
|
||||||
|
errorText,
|
||||||
|
maxHeight = 300,
|
||||||
|
}: ToolCallDetailsContentProps) {
|
||||||
|
// Compute diff lines for edit type
|
||||||
|
const diffLines = useMemo(() => {
|
||||||
|
if (display.type !== "edit") return undefined;
|
||||||
|
return buildLineDiff(display.oldString, display.newString);
|
||||||
|
}, [display]);
|
||||||
|
|
||||||
|
const sections: ReactNode[] = [];
|
||||||
|
|
||||||
|
if (display.type === "shell") {
|
||||||
|
sections.push(
|
||||||
|
<View key="shell" style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>Command</Text>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
nestedScrollEnabled
|
||||||
|
style={styles.jsonScroll}
|
||||||
|
contentContainerStyle={styles.jsonContent}
|
||||||
|
showsHorizontalScrollIndicator={true}
|
||||||
|
>
|
||||||
|
<Text style={styles.scrollText}>{display.command}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
{display.output ? (
|
||||||
|
<ScrollView
|
||||||
|
style={[styles.scrollArea, { maxHeight }]}
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
nestedScrollEnabled
|
||||||
|
showsVerticalScrollIndicator={true}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
nestedScrollEnabled
|
||||||
|
showsHorizontalScrollIndicator={true}
|
||||||
|
>
|
||||||
|
<Text style={styles.scrollText}>{display.output}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
</ScrollView>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
} else if (display.type === "edit") {
|
||||||
|
sections.push(
|
||||||
|
<View key="edit" style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>File</Text>
|
||||||
|
<View style={styles.fileBadge}>
|
||||||
|
<Text style={styles.fileBadgeText}>{display.filePath}</Text>
|
||||||
|
</View>
|
||||||
|
{diffLines && diffLines.length > 0 ? (
|
||||||
|
<View style={styles.diffContainer}>
|
||||||
|
<DiffViewer diffLines={diffLines} maxHeight={maxHeight} />
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
} else if (display.type === "read") {
|
||||||
|
sections.push(
|
||||||
|
<View key="read" style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>File</Text>
|
||||||
|
<View style={styles.fileBadge}>
|
||||||
|
<Text style={styles.fileBadgeText}>{display.filePath}</Text>
|
||||||
|
</View>
|
||||||
|
{(display.offset !== undefined || display.limit !== undefined) ? (
|
||||||
|
<Text style={styles.rangeText}>
|
||||||
|
{display.offset !== undefined ? `Offset: ${display.offset}` : ""}
|
||||||
|
{display.offset !== undefined && display.limit !== undefined ? " • " : ""}
|
||||||
|
{display.limit !== undefined ? `Limit: ${display.limit}` : ""}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{display.content ? (
|
||||||
|
<ScrollView
|
||||||
|
style={[styles.scrollArea, { maxHeight }]}
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
nestedScrollEnabled
|
||||||
|
showsVerticalScrollIndicator={true}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
nestedScrollEnabled
|
||||||
|
showsHorizontalScrollIndicator={true}
|
||||||
|
>
|
||||||
|
<Text style={styles.scrollText}>{display.content}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
</ScrollView>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Generic tool: show input/output as key-value pairs
|
||||||
|
if (display.input.length > 0) {
|
||||||
|
sections.push(
|
||||||
|
<View key="input-header" style={styles.groupHeader}>
|
||||||
|
<Text style={styles.groupHeaderText}>Input</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
display.input.forEach((pair, index) => {
|
||||||
|
sections.push(
|
||||||
|
<View key={`input-${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>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (display.output.length > 0) {
|
||||||
|
sections.push(
|
||||||
|
<View key="output-header" style={styles.groupHeader}>
|
||||||
|
<Text style={styles.groupHeaderText}>Output</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
display.output.forEach((pair, index) => {
|
||||||
|
sections.push(
|
||||||
|
<View key={`output-${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>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show errors if available
|
||||||
|
if (errorText) {
|
||||||
|
sections.push(
|
||||||
|
<View key="error" style={styles.section}>
|
||||||
|
<Text style={[styles.sectionTitle, styles.errorText]}>Error</Text>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
nestedScrollEnabled
|
||||||
|
style={[styles.jsonScroll, styles.jsonScrollError]}
|
||||||
|
contentContainerStyle={styles.jsonContent}
|
||||||
|
showsHorizontalScrollIndicator={true}
|
||||||
|
>
|
||||||
|
<Text style={[styles.scrollText, styles.errorText]}>
|
||||||
|
{errorText}
|
||||||
|
</Text>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sections.length === 0) {
|
||||||
|
return (
|
||||||
|
<Text style={styles.emptyStateText}>No additional details available</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <View style={styles.container}>{sections}</View>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Hook for parsing tool call data ----
|
||||||
|
|
||||||
|
export function useToolCallDetails(data: ToolCallDetailsData) {
|
||||||
|
const { args, result, error } = data;
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const display = parseToolCallDisplay(args, result);
|
||||||
|
const errorText = error !== undefined ? formatValue(error) : undefined;
|
||||||
|
return { display, errorText };
|
||||||
|
}, [args, result, error]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Styles ----
|
||||||
|
|
||||||
|
const styles = StyleSheet.create((theme) => ({
|
||||||
|
container: {
|
||||||
|
gap: theme.spacing[4],
|
||||||
|
},
|
||||||
|
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],
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
fontWeight: theme.fontWeight.semibold,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
fileBadge: {
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
paddingHorizontal: theme.spacing[2],
|
||||||
|
paddingVertical: theme.spacing[1],
|
||||||
|
borderRadius: theme.borderRadius.base,
|
||||||
|
borderWidth: theme.borderWidth[1],
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
backgroundColor: theme.colors.card,
|
||||||
|
},
|
||||||
|
fileBadgeText: {
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
fontFamily: Fonts.mono,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
},
|
||||||
|
rangeText: {
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
},
|
||||||
|
diffContainer: {
|
||||||
|
borderWidth: theme.borderWidth[1],
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
borderRadius: theme.borderRadius.base,
|
||||||
|
overflow: "hidden",
|
||||||
|
backgroundColor: theme.colors.card,
|
||||||
|
},
|
||||||
|
scrollArea: {
|
||||||
|
borderWidth: theme.borderWidth[1],
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
borderRadius: theme.borderRadius.base,
|
||||||
|
backgroundColor: theme.colors.card,
|
||||||
|
},
|
||||||
|
scrollContent: {
|
||||||
|
padding: theme.spacing[2],
|
||||||
|
},
|
||||||
|
scrollText: {
|
||||||
|
fontFamily: Fonts.mono,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
lineHeight: 18,
|
||||||
|
},
|
||||||
|
jsonScroll: {
|
||||||
|
borderWidth: theme.borderWidth[1],
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
borderRadius: theme.borderRadius.base,
|
||||||
|
backgroundColor: theme.colors.card,
|
||||||
|
},
|
||||||
|
jsonScrollError: {
|
||||||
|
borderColor: theme.colors.destructive,
|
||||||
|
},
|
||||||
|
jsonContent: {
|
||||||
|
padding: theme.spacing[2],
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: theme.colors.destructive,
|
||||||
|
},
|
||||||
|
emptyStateText: {
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
fontStyle: "italic",
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -7,17 +7,14 @@ import React, {
|
|||||||
ReactNode,
|
ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { View, Text, Pressable } from "react-native";
|
import { View, Text, Pressable } from "react-native";
|
||||||
import { ScrollView } from "react-native-gesture-handler";
|
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { Fonts } from "@/constants/theme";
|
|
||||||
import {
|
import {
|
||||||
BottomSheetModal,
|
BottomSheetModal,
|
||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
BottomSheetBackdrop,
|
BottomSheetBackdrop,
|
||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import { Pencil, Eye, SquareTerminal, Search, Wrench, X } from "lucide-react-native";
|
import { Pencil, Eye, SquareTerminal, Search, Wrench, X } from "lucide-react-native";
|
||||||
import { parseToolCallDisplay, buildLineDiff, type DiffLine } from "@/utils/tool-call-parsers";
|
import { ToolCallDetailsContent, useToolCallDetails } from "./tool-call-details";
|
||||||
import { DiffViewer } from "./diff-viewer";
|
|
||||||
|
|
||||||
// ----- Types -----
|
// ----- Types -----
|
||||||
|
|
||||||
@@ -56,22 +53,6 @@ const toolKindIcons: Record<string, React.ComponentType<{ size?: number; color?:
|
|||||||
search: Search,
|
search: Search,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----- Helper Functions -----
|
|
||||||
|
|
||||||
function formatValue(value: unknown): string {
|
|
||||||
if (value === undefined) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.stringify(value, null, 2);
|
|
||||||
} catch {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- Provider Component -----
|
// ----- Provider Component -----
|
||||||
|
|
||||||
interface ToolCallSheetProviderProps {
|
interface ToolCallSheetProviderProps {
|
||||||
@@ -150,184 +131,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
|||||||
? toolKindIcons[kind.toLowerCase()] || Wrench
|
? toolKindIcons[kind.toLowerCase()] || Wrench
|
||||||
: Wrench;
|
: Wrench;
|
||||||
|
|
||||||
const serializedError = useMemo(
|
const { display, errorText } = useToolCallDetails({ args, result, error });
|
||||||
() => (error !== undefined ? formatValue(error) : ""),
|
|
||||||
[error]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Parse tool call display using discriminated union
|
|
||||||
const toolCallDisplay = useMemo(
|
|
||||||
() => parseToolCallDisplay(args, result),
|
|
||||||
[args, result]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Compute diff lines for edit type
|
|
||||||
const editDiffLines = useMemo((): DiffLine[] => {
|
|
||||||
if (toolCallDisplay.type !== "edit") return [];
|
|
||||||
return buildLineDiff(toolCallDisplay.oldString, toolCallDisplay.newString);
|
|
||||||
}, [toolCallDisplay]);
|
|
||||||
|
|
||||||
// Render content sections
|
|
||||||
const renderSections = useCallback(() => {
|
|
||||||
const sections: ReactNode[] = [];
|
|
||||||
|
|
||||||
if (toolCallDisplay.type === "shell") {
|
|
||||||
// Shell tool: show command and output as single block
|
|
||||||
sections.push(
|
|
||||||
<View key="shell" style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>Command</Text>
|
|
||||||
<ScrollView
|
|
||||||
horizontal
|
|
||||||
nestedScrollEnabled
|
|
||||||
style={styles.jsonScroll}
|
|
||||||
contentContainerStyle={styles.jsonContent}
|
|
||||||
showsHorizontalScrollIndicator={true}
|
|
||||||
>
|
|
||||||
<Text style={styles.scrollText}>{toolCallDisplay.command}</Text>
|
|
||||||
</ScrollView>
|
|
||||||
{toolCallDisplay.output ? (
|
|
||||||
<ScrollView
|
|
||||||
style={styles.scrollArea}
|
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
nestedScrollEnabled
|
|
||||||
showsVerticalScrollIndicator={true}
|
|
||||||
>
|
|
||||||
<ScrollView
|
|
||||||
horizontal
|
|
||||||
nestedScrollEnabled
|
|
||||||
showsHorizontalScrollIndicator={true}
|
|
||||||
>
|
|
||||||
<Text style={styles.scrollText}>{toolCallDisplay.output}</Text>
|
|
||||||
</ScrollView>
|
|
||||||
</ScrollView>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
} else if (toolCallDisplay.type === "edit") {
|
|
||||||
// Edit tool: show file path and diff
|
|
||||||
sections.push(
|
|
||||||
<View key="edit" style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>File</Text>
|
|
||||||
<View style={styles.fileBadge}>
|
|
||||||
<Text style={styles.fileBadgeText}>{toolCallDisplay.filePath}</Text>
|
|
||||||
</View>
|
|
||||||
{editDiffLines.length > 0 ? (
|
|
||||||
<View style={styles.diffContainer}>
|
|
||||||
<DiffViewer diffLines={editDiffLines} maxHeight={300} />
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
} else if (toolCallDisplay.type === "read") {
|
|
||||||
// Read tool: show file path and content
|
|
||||||
sections.push(
|
|
||||||
<View key="read" style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>File</Text>
|
|
||||||
<View style={styles.fileBadge}>
|
|
||||||
<Text style={styles.fileBadgeText}>{toolCallDisplay.filePath}</Text>
|
|
||||||
</View>
|
|
||||||
{(toolCallDisplay.offset !== undefined || toolCallDisplay.limit !== undefined) ? (
|
|
||||||
<Text style={styles.rangeText}>
|
|
||||||
{toolCallDisplay.offset !== undefined ? `Offset: ${toolCallDisplay.offset}` : ""}
|
|
||||||
{toolCallDisplay.offset !== undefined && toolCallDisplay.limit !== undefined ? " • " : ""}
|
|
||||||
{toolCallDisplay.limit !== undefined ? `Limit: ${toolCallDisplay.limit}` : ""}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
{toolCallDisplay.content ? (
|
|
||||||
<ScrollView
|
|
||||||
style={styles.scrollArea}
|
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
nestedScrollEnabled
|
|
||||||
showsVerticalScrollIndicator={true}
|
|
||||||
>
|
|
||||||
<ScrollView
|
|
||||||
horizontal
|
|
||||||
nestedScrollEnabled
|
|
||||||
showsHorizontalScrollIndicator={true}
|
|
||||||
>
|
|
||||||
<Text style={styles.scrollText}>{toolCallDisplay.content}</Text>
|
|
||||||
</ScrollView>
|
|
||||||
</ScrollView>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Generic tool: show input/output as key-value pairs
|
|
||||||
if (toolCallDisplay.input.length > 0) {
|
|
||||||
sections.push(
|
|
||||||
<View key="input-header" style={styles.groupHeader}>
|
|
||||||
<Text style={styles.groupHeaderText}>Input</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
toolCallDisplay.input.forEach((pair, index) => {
|
|
||||||
sections.push(
|
|
||||||
<View key={`input-${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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolCallDisplay.output.length > 0) {
|
|
||||||
sections.push(
|
|
||||||
<View key="output-header" style={styles.groupHeader}>
|
|
||||||
<Text style={styles.groupHeaderText}>Output</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
toolCallDisplay.output.forEach((pair, index) => {
|
|
||||||
sections.push(
|
|
||||||
<View key={`output-${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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always show errors if available
|
|
||||||
if (error !== undefined) {
|
|
||||||
sections.push(
|
|
||||||
<View key="error" style={styles.section}>
|
|
||||||
<Text style={[styles.sectionTitle, styles.errorText]}>Error</Text>
|
|
||||||
<ScrollView
|
|
||||||
horizontal
|
|
||||||
nestedScrollEnabled
|
|
||||||
style={[styles.jsonScroll, styles.jsonScrollError]}
|
|
||||||
contentContainerStyle={styles.jsonContent}
|
|
||||||
showsHorizontalScrollIndicator={true}
|
|
||||||
>
|
|
||||||
<Text style={[styles.scrollText, styles.errorText]}>{serializedError}</Text>
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sections.length === 0) {
|
|
||||||
return (
|
|
||||||
<Text style={styles.emptyStateText}>No additional details available</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sections;
|
|
||||||
}, [toolCallDisplay, editDiffLines, error, serializedError]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@@ -349,7 +153,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
|||||||
style={styles.content}
|
style={styles.content}
|
||||||
contentContainerStyle={styles.contentContainer}
|
contentContainerStyle={styles.contentContainer}
|
||||||
>
|
>
|
||||||
{renderSections()}
|
<ToolCallDetailsContent display={display} errorText={errorText} />
|
||||||
</BottomSheetScrollView>
|
</BottomSheetScrollView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -406,92 +210,5 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
paddingHorizontal: theme.spacing[4],
|
paddingHorizontal: theme.spacing[4],
|
||||||
paddingTop: theme.spacing[4],
|
paddingTop: theme.spacing[4],
|
||||||
paddingBottom: theme.spacing[8],
|
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],
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
|
||||||
color: theme.colors.mutedForeground,
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
fontWeight: theme.fontWeight.semibold,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
|
||||||
fileBadge: {
|
|
||||||
alignSelf: "flex-start",
|
|
||||||
paddingHorizontal: theme.spacing[2],
|
|
||||||
paddingVertical: theme.spacing[1],
|
|
||||||
borderRadius: theme.borderRadius.base,
|
|
||||||
borderWidth: theme.borderWidth[1],
|
|
||||||
borderColor: theme.colors.border,
|
|
||||||
backgroundColor: theme.colors.card,
|
|
||||||
},
|
|
||||||
fileBadgeText: {
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
fontFamily: Fonts.mono,
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
},
|
|
||||||
rangeText: {
|
|
||||||
color: theme.colors.mutedForeground,
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
},
|
|
||||||
diffContainer: {
|
|
||||||
borderWidth: theme.borderWidth[1],
|
|
||||||
borderColor: theme.colors.border,
|
|
||||||
borderRadius: theme.borderRadius.base,
|
|
||||||
overflow: "hidden",
|
|
||||||
backgroundColor: theme.colors.card,
|
|
||||||
},
|
|
||||||
scrollArea: {
|
|
||||||
borderWidth: theme.borderWidth[1],
|
|
||||||
borderColor: theme.colors.border,
|
|
||||||
borderRadius: theme.borderRadius.base,
|
|
||||||
maxHeight: 260,
|
|
||||||
backgroundColor: theme.colors.card,
|
|
||||||
},
|
|
||||||
scrollContent: {
|
|
||||||
padding: theme.spacing[2],
|
|
||||||
},
|
|
||||||
scrollText: {
|
|
||||||
fontFamily: Fonts.mono,
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
lineHeight: 18,
|
|
||||||
},
|
|
||||||
jsonScroll: {
|
|
||||||
borderWidth: theme.borderWidth[1],
|
|
||||||
borderColor: theme.colors.border,
|
|
||||||
borderRadius: theme.borderRadius.base,
|
|
||||||
backgroundColor: theme.colors.card,
|
|
||||||
},
|
|
||||||
jsonScrollError: {
|
|
||||||
borderColor: theme.colors.destructive,
|
|
||||||
},
|
|
||||||
jsonContent: {
|
|
||||||
padding: theme.spacing[2],
|
|
||||||
},
|
|
||||||
errorText: {
|
|
||||||
color: theme.colors.destructive,
|
|
||||||
},
|
|
||||||
emptyStateText: {
|
|
||||||
color: theme.colors.mutedForeground,
|
|
||||||
fontSize: theme.fontSize.sm,
|
|
||||||
fontStyle: "italic",
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user