Unified tool call display schema, compaction timeline, and auth fixes
- Replace extractPrincipalParam, parseToolCallDisplay, getToolKindFromName with a single unified parseToolCallDisplay on the server, consumed by both server (activity-curator) and app - Parse tool calls exactly once and pass ToolCallDisplayInfo down to all consumers (message, sheet, details, stream view) - Add compaction timeline item: detect SDKStatusMessage (compacting) and SDKCompactBoundaryMessage, show loading/completed marker in timeline - Suppress compact summary user message in live stream (flag) and JSONL resume (isCompactSummary check) - Add CompactionMarker component with scissors icon and token count - Hide sub-agent tool calls from timeline, collapse into Task metadata - Add Bot icon for Task tool calls - Fix stripShellWrapperPrefix to strip /bin/zsh -lc wrappers without cd - Replace CLAUDE_SESSION_TOKEN with CLAUDE_CODE_OAUTH_TOKEN
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
||||
ActivityLog,
|
||||
ToolCall,
|
||||
TodoListCard,
|
||||
CompactionMarker,
|
||||
TurnCopyButton,
|
||||
MessageOuterSpacingProvider,
|
||||
type InlinePathTarget,
|
||||
@@ -366,6 +367,7 @@ export function AgentStreamView({
|
||||
error={data.error}
|
||||
status={data.status as "executing" | "completed" | "failed"}
|
||||
cwd={agent.cwd}
|
||||
metadata={data.metadata}
|
||||
isLastInSequence={isLastInSequence}
|
||||
onInlineDetailsExpandedChange={handleInlineDetailsExpandedChange}
|
||||
/>
|
||||
@@ -402,6 +404,14 @@ export function AgentStreamView({
|
||||
/>
|
||||
);
|
||||
|
||||
case "compaction":
|
||||
return (
|
||||
<CompactionMarker
|
||||
status={item.status}
|
||||
preTokens={item.preTokens}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -897,7 +907,7 @@ function PermissionRequestCard({
|
||||
if (isPlanRequest) {
|
||||
return null;
|
||||
}
|
||||
return parseToolCallDisplay(request.name ?? "unknown", request.input, null);
|
||||
return parseToolCallDisplay({ name: request.name ?? "unknown", input: request.input });
|
||||
}, [isPlanRequest, request.name, request.input]);
|
||||
|
||||
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
|
||||
@@ -1116,7 +1126,7 @@ function PermissionRequestCard({
|
||||
) : null}
|
||||
|
||||
{!isPlanRequest && toolCallDisplay ? (
|
||||
<ToolCallDetailsContent display={toolCallDisplay} maxHeight={200} />
|
||||
<ToolCallDetailsContent detail={toolCallDisplay.detail} maxHeight={200} />
|
||||
) : null}
|
||||
|
||||
<Text
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Text,
|
||||
Pressable,
|
||||
Animated,
|
||||
ActivityIndicator,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
Platform,
|
||||
@@ -38,8 +39,10 @@ import {
|
||||
SquareTerminal,
|
||||
Search,
|
||||
Brain,
|
||||
Bot,
|
||||
Copy,
|
||||
TriangleAlertIcon,
|
||||
Scissors,
|
||||
} from "lucide-react-native";
|
||||
import {
|
||||
StyleSheet,
|
||||
@@ -53,16 +56,13 @@ import {
|
||||
import { Colors, Fonts } from "@/constants/theme";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import type { TodoEntry } from "@/types/stream";
|
||||
import { extractPrincipalParam } from "@/utils/tool-call-parsers";
|
||||
import { parseToolCallDisplay, type ToolCallKind } from "@/utils/tool-call-parsers";
|
||||
import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
|
||||
import { parseInlinePathToken, type InlinePathTarget } from "@/utils/inline-path";
|
||||
export type { InlinePathTarget } from "@/utils/inline-path";
|
||||
import { resolveToolCallPreview } from "./tool-call-preview";
|
||||
import { useToolCallSheet } from "./tool-call-sheet";
|
||||
import {
|
||||
ToolCallDetailsContent,
|
||||
useToolCallDetails,
|
||||
} from "./tool-call-details";
|
||||
import { ToolCallDetailsContent } from "./tool-call-details";
|
||||
|
||||
interface UserMessageProps {
|
||||
message: string;
|
||||
@@ -767,6 +767,63 @@ export const ActivityLog = memo(function ActivityLog({
|
||||
);
|
||||
});
|
||||
|
||||
interface CompactionMarkerProps {
|
||||
status: "loading" | "completed";
|
||||
preTokens?: number;
|
||||
}
|
||||
|
||||
const compactionStylesheet = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
line: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: theme.colors.border,
|
||||
},
|
||||
label: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
text: {
|
||||
fontFamily: Fonts.mono,
|
||||
fontSize: 11,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
}));
|
||||
|
||||
export const CompactionMarker = memo(function CompactionMarker({
|
||||
status,
|
||||
preTokens,
|
||||
}: CompactionMarkerProps) {
|
||||
const label =
|
||||
status === "loading"
|
||||
? "Compacting..."
|
||||
: preTokens
|
||||
? `Context compacted (${Math.round(preTokens / 1000)}K tokens)`
|
||||
: "Context compacted";
|
||||
|
||||
return (
|
||||
<View style={compactionStylesheet.container}>
|
||||
<View style={compactionStylesheet.line} />
|
||||
<View style={compactionStylesheet.label}>
|
||||
{status === "loading" ? (
|
||||
<ActivityIndicator size="small" color="#a1a1aa" />
|
||||
) : (
|
||||
<Scissors size={12} color="#a1a1aa" />
|
||||
)}
|
||||
<Text style={compactionStylesheet.text}>{label}</Text>
|
||||
</View>
|
||||
<View style={compactionStylesheet.line} />
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
interface TodoListCardProps {
|
||||
items: TodoEntry[];
|
||||
disableOuterSpacing?: boolean;
|
||||
@@ -1038,6 +1095,7 @@ interface ToolCallProps {
|
||||
error?: any;
|
||||
status: "executing" | "completed" | "failed";
|
||||
cwd?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
isLastInSequence?: boolean;
|
||||
disableOuterSpacing?: boolean;
|
||||
onInlineDetailsHoverChange?: (hovered: boolean) => void;
|
||||
@@ -1051,23 +1109,11 @@ const toolKindIcons: Record<string, any> = {
|
||||
execute: SquareTerminal,
|
||||
search: Search,
|
||||
thinking: Brain,
|
||||
agent: Bot,
|
||||
};
|
||||
const TOOL_CALL_LOG_TAG = "[ToolCall]";
|
||||
const TOOL_CALL_COMMIT_THRESHOLD_MS = 16;
|
||||
|
||||
// Derive tool kind from tool name for icon selection
|
||||
function getToolKindFromName(toolName: string): string {
|
||||
const lower = toolName.toLowerCase();
|
||||
if (lower === "thinking") return "thinking";
|
||||
if (lower === "read" || lower === "read_file" || lower.startsWith("read"))
|
||||
return "read";
|
||||
if (lower === "edit" || lower === "write" || lower === "apply_patch")
|
||||
return "edit";
|
||||
if (lower === "bash" || lower === "shell") return "execute";
|
||||
if (lower === "grep" || lower === "glob" || lower === "web_search")
|
||||
return "search";
|
||||
return "tool";
|
||||
}
|
||||
|
||||
export const ToolCall = memo(function ToolCall({
|
||||
toolName,
|
||||
@@ -1076,6 +1122,7 @@ export const ToolCall = memo(function ToolCall({
|
||||
error,
|
||||
status,
|
||||
cwd,
|
||||
metadata,
|
||||
isLastInSequence = false,
|
||||
disableOuterSpacing,
|
||||
onInlineDetailsHoverChange,
|
||||
@@ -1090,42 +1137,27 @@ export const ToolCall = memo(function ToolCall({
|
||||
UnistylesRuntime.breakpoint === "xs" ||
|
||||
UnistylesRuntime.breakpoint === "sm";
|
||||
|
||||
const kind = getToolKindFromName(toolName);
|
||||
const IconComponent = toolKindIcons[kind] || Wrench;
|
||||
|
||||
// Extract principal param for secondary label (memoized)
|
||||
const principalParam = useMemo(
|
||||
() => extractPrincipalParam(args, cwd),
|
||||
[args, cwd]
|
||||
const displayInfo = useMemo(
|
||||
() => parseToolCallDisplay({ name: toolName, input: args, output: result, error, metadata, cwd }),
|
||||
[toolName, args, result, error, metadata, cwd]
|
||||
);
|
||||
const { kind, displayName, summary, detail, errorText } = displayInfo;
|
||||
const IconComponent = toolKindIcons[kind] || Wrench;
|
||||
|
||||
// Check if there's any content to display
|
||||
const hasDetails =
|
||||
args !== undefined || result !== undefined || error !== undefined;
|
||||
|
||||
// Parse tool call details for inline rendering
|
||||
const { display, errorText } = useToolCallDetails({ toolName, args, result, error });
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (!isMobile && isPerfLoggingEnabled()) {
|
||||
toggleStartRef.current = getNowMs();
|
||||
}
|
||||
if (isMobile) {
|
||||
// Mobile: open bottom sheet
|
||||
openToolCall({
|
||||
toolName,
|
||||
kind,
|
||||
status,
|
||||
args,
|
||||
result,
|
||||
error,
|
||||
cwd,
|
||||
});
|
||||
openToolCall(displayInfo);
|
||||
} else {
|
||||
// Desktop: toggle inline expansion
|
||||
setIsExpanded((prev) => !prev);
|
||||
}
|
||||
}, [isMobile, openToolCall, toolName, kind, status, args, result, error, cwd]);
|
||||
}, [isMobile, openToolCall, displayInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile || !isPerfLoggingEnabled()) {
|
||||
@@ -1184,14 +1216,14 @@ export const ToolCall = memo(function ToolCall({
|
||||
// Render inline details for desktop
|
||||
const renderDetails = useCallback(() => {
|
||||
if (isMobile) return null;
|
||||
return <ToolCallDetailsContent display={display} errorText={errorText} maxHeight={400} />;
|
||||
}, [isMobile, display, errorText]);
|
||||
return <ToolCallDetailsContent detail={detail} errorText={errorText} maxHeight={400} />;
|
||||
}, [isMobile, detail, errorText]);
|
||||
|
||||
return (
|
||||
<ExpandableBadge
|
||||
testID="tool-call-badge"
|
||||
label={display.toolName}
|
||||
secondaryLabel={principalParam}
|
||||
label={displayName}
|
||||
secondaryLabel={summary}
|
||||
icon={IconComponent}
|
||||
isExpanded={!isMobile && isExpanded}
|
||||
onToggle={hasDetails ? handleToggle : undefined}
|
||||
|
||||
@@ -3,169 +3,46 @@ import { View, Text, Platform, ScrollView as RNScrollView } from "react-native";
|
||||
import { ScrollView as GHScrollView } from "react-native-gesture-handler";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { Fonts } from "@/constants/theme";
|
||||
import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
|
||||
import {
|
||||
parseToolCallDisplay,
|
||||
buildLineDiff,
|
||||
parseUnifiedDiff,
|
||||
type ToolCallDisplay,
|
||||
type ToolCallDetail,
|
||||
} from "@/utils/tool-call-parsers";
|
||||
import { DiffViewer } from "./diff-viewer";
|
||||
import { getCodeInsets } from "./code-insets";
|
||||
|
||||
const ScrollView = Platform.OS === "web" ? RNScrollView : GHScrollView;
|
||||
|
||||
// ---- Types ----
|
||||
|
||||
export interface ToolCallDetailsData {
|
||||
toolName: string;
|
||||
args?: unknown;
|
||||
result?: unknown;
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
const TOOL_CALL_DETAILS_LOG_TAG = "[ToolCallDetails]";
|
||||
const TOOL_CALL_DETAILS_DURATION_THRESHOLD_MS = 8;
|
||||
const TOOL_CALL_DETAILS_SIZE_THRESHOLD = 20000;
|
||||
|
||||
type ToolCallDisplaySummary = {
|
||||
displayType: ToolCallDisplay["type"];
|
||||
totalChars: number;
|
||||
detail: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function summarizeToolCallDisplay(display: ToolCallDisplay): ToolCallDisplaySummary {
|
||||
switch (display.type) {
|
||||
case "shell": {
|
||||
const commandLength = display.command.length;
|
||||
const outputLength = display.output.length;
|
||||
return {
|
||||
displayType: display.type,
|
||||
totalChars: commandLength + outputLength,
|
||||
detail: {
|
||||
commandLength,
|
||||
outputLength,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "edit": {
|
||||
const oldLength = display.oldString.length;
|
||||
const newLength = display.newString.length;
|
||||
return {
|
||||
displayType: display.type,
|
||||
totalChars: oldLength + newLength,
|
||||
detail: {
|
||||
filePath: display.filePath,
|
||||
oldLength,
|
||||
newLength,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "read": {
|
||||
const contentLength = display.content.length;
|
||||
return {
|
||||
displayType: display.type,
|
||||
totalChars: contentLength,
|
||||
detail: {
|
||||
filePath: display.filePath,
|
||||
contentLength,
|
||||
offset: display.offset,
|
||||
limit: display.limit,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "generic": {
|
||||
const inputPairs = display.input.length;
|
||||
const outputPairs = display.output.length;
|
||||
const inputChars = display.input.reduce((sum, pair) => sum + pair.value.length, 0);
|
||||
const outputChars = display.output.reduce((sum, pair) => sum + pair.value.length, 0);
|
||||
return {
|
||||
displayType: display.type,
|
||||
totalChars: inputChars + outputChars,
|
||||
detail: {
|
||||
inputPairs,
|
||||
outputPairs,
|
||||
inputChars,
|
||||
outputChars,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "thinking": {
|
||||
const contentLength = display.content.length;
|
||||
return {
|
||||
displayType: display.type,
|
||||
totalChars: contentLength,
|
||||
detail: { contentLength },
|
||||
};
|
||||
}
|
||||
default:
|
||||
return assertNever(display);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Helper ----
|
||||
|
||||
function assertNever(value: never): never {
|
||||
throw new Error(`Unhandled tool call display: ${JSON.stringify(value)}`);
|
||||
}
|
||||
|
||||
function formatValue(value: unknown): string {
|
||||
if (value === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
// Extract content from tool_result objects
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"type" in value &&
|
||||
(value as { type: string }).type === "tool_result" &&
|
||||
"content" in value
|
||||
) {
|
||||
const content = (value as { content: unknown }).content;
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Content Component ----
|
||||
|
||||
interface ToolCallDetailsContentProps {
|
||||
display: ToolCallDisplay;
|
||||
detail: ToolCallDetail;
|
||||
errorText?: string;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
export function ToolCallDetailsContent({
|
||||
display,
|
||||
detail,
|
||||
errorText,
|
||||
maxHeight = 300,
|
||||
}: ToolCallDetailsContentProps) {
|
||||
// Compute diff lines for edit type
|
||||
const diffLines = useMemo(() => {
|
||||
if (display.type !== "edit") return undefined;
|
||||
if (detail.type !== "edit") return undefined;
|
||||
// Use pre-computed unified diff if available (e.g., from apply_patch)
|
||||
if (display.unifiedDiff) {
|
||||
return parseUnifiedDiff(display.unifiedDiff);
|
||||
if (detail.unifiedDiff) {
|
||||
return parseUnifiedDiff(detail.unifiedDiff);
|
||||
}
|
||||
return buildLineDiff(display.oldString, display.newString);
|
||||
}, [display]);
|
||||
return buildLineDiff(detail.oldString, detail.newString);
|
||||
}, [detail]);
|
||||
|
||||
const sections: ReactNode[] = [];
|
||||
const isFullBleed = display.type === "edit" || display.type === "shell";
|
||||
const isFullBleed = detail.type === "edit" || detail.type === "shell";
|
||||
const codeBlockStyle = isFullBleed ? styles.fullBleedBlock : styles.diffContainer;
|
||||
|
||||
if (display.type === "shell") {
|
||||
const command = display.command.replace(/\n+$/, "");
|
||||
const output = display.output.replace(/^\n+/, "");
|
||||
if (detail.type === "shell") {
|
||||
const command = detail.command.replace(/\n+$/, "");
|
||||
const output = detail.output.replace(/^\n+/, "");
|
||||
const hasOutput = output.length > 0;
|
||||
sections.push(
|
||||
<View key="shell" style={styles.section}>
|
||||
@@ -194,7 +71,7 @@ export function ToolCallDetailsContent({
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
} else if (display.type === "edit") {
|
||||
} else if (detail.type === "edit") {
|
||||
sections.push(
|
||||
<View key="edit" style={styles.section}>
|
||||
{diffLines ? (
|
||||
@@ -204,17 +81,17 @@ export function ToolCallDetailsContent({
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
} else if (display.type === "read") {
|
||||
} else if (detail.type === "read") {
|
||||
sections.push(
|
||||
<View key="read" style={styles.section}>
|
||||
{(display.offset !== undefined || display.limit !== undefined) ? (
|
||||
{(detail.offset !== undefined || detail.limit !== undefined) ? (
|
||||
<Text style={styles.rangeText}>
|
||||
{display.offset !== undefined ? `Offset: ${display.offset}` : ""}
|
||||
{display.offset !== undefined && display.limit !== undefined ? " • " : ""}
|
||||
{display.limit !== undefined ? `Limit: ${display.limit}` : ""}
|
||||
{detail.offset !== undefined ? `Offset: ${detail.offset}` : ""}
|
||||
{detail.offset !== undefined && detail.limit !== undefined ? " • " : ""}
|
||||
{detail.limit !== undefined ? `Limit: ${detail.limit}` : ""}
|
||||
</Text>
|
||||
) : null}
|
||||
{display.content ? (
|
||||
{detail.content ? (
|
||||
<ScrollView
|
||||
style={[styles.scrollArea, { maxHeight }]}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
@@ -226,13 +103,13 @@ export function ToolCallDetailsContent({
|
||||
nestedScrollEnabled
|
||||
showsHorizontalScrollIndicator={true}
|
||||
>
|
||||
<Text selectable style={styles.scrollText}>{display.content}</Text>
|
||||
<Text selectable style={styles.scrollText}>{detail.content}</Text>
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
} else if (display.type === "thinking") {
|
||||
} else if (detail.type === "thinking") {
|
||||
// Thinking: display the content as plain text
|
||||
sections.push(
|
||||
<View key="thinking" style={styles.section}>
|
||||
@@ -242,19 +119,19 @@ export function ToolCallDetailsContent({
|
||||
nestedScrollEnabled
|
||||
showsVerticalScrollIndicator={true}
|
||||
>
|
||||
<Text selectable style={styles.scrollText}>{display.content}</Text>
|
||||
<Text selectable style={styles.scrollText}>{detail.content}</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
// Generic tool: show input/output as key-value pairs
|
||||
if (display.input.length > 0) {
|
||||
if (detail.input.length > 0) {
|
||||
sections.push(
|
||||
<View key="input-header" style={styles.groupHeader}>
|
||||
<Text style={styles.groupHeaderText}>Input</Text>
|
||||
</View>
|
||||
);
|
||||
display.input.forEach((pair, index) => {
|
||||
detail.input.forEach((pair, index) => {
|
||||
sections.push(
|
||||
<View key={`input-${index}-${pair.key}`} style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{pair.key}</Text>
|
||||
@@ -272,13 +149,13 @@ export function ToolCallDetailsContent({
|
||||
});
|
||||
}
|
||||
|
||||
if (display.output.length > 0) {
|
||||
if (detail.output.length > 0) {
|
||||
sections.push(
|
||||
<View key="output-header" style={styles.groupHeader}>
|
||||
<Text style={styles.groupHeaderText}>Output</Text>
|
||||
</View>
|
||||
);
|
||||
display.output.forEach((pair, index) => {
|
||||
detail.output.forEach((pair, index) => {
|
||||
sections.push(
|
||||
<View key={`output-${index}-${pair.key}`} style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{pair.key}</Text>
|
||||
@@ -330,37 +207,6 @@ export function ToolCallDetailsContent({
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Hook for parsing tool call data ----
|
||||
|
||||
export function useToolCallDetails(data: ToolCallDetailsData) {
|
||||
const { toolName, args, result, error } = data;
|
||||
|
||||
return useMemo(() => {
|
||||
const shouldLog = isPerfLoggingEnabled();
|
||||
const startMs = shouldLog ? getNowMs() : 0;
|
||||
const display = parseToolCallDisplay(toolName, args, result);
|
||||
const errorText = error !== undefined ? formatValue(error) : undefined;
|
||||
if (shouldLog) {
|
||||
const durationMs = getNowMs() - startMs;
|
||||
const summary = summarizeToolCallDisplay(display);
|
||||
if (
|
||||
durationMs >= TOOL_CALL_DETAILS_DURATION_THRESHOLD_MS ||
|
||||
summary.totalChars >= TOOL_CALL_DETAILS_SIZE_THRESHOLD
|
||||
) {
|
||||
perfLog(TOOL_CALL_DETAILS_LOG_TAG, {
|
||||
event: "parse",
|
||||
durationMs: Math.round(durationMs),
|
||||
displayType: summary.displayType,
|
||||
totalChars: summary.totalChars,
|
||||
errorLength: errorText ? errorText.length : 0,
|
||||
...summary.detail,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { display, errorText };
|
||||
}, [toolName, args, result, error]);
|
||||
}
|
||||
|
||||
// ---- Styles ----
|
||||
|
||||
const styles = StyleSheet.create((theme) => {
|
||||
|
||||
@@ -15,21 +15,13 @@ import {
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetBackgroundProps,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { Pencil, Eye, SquareTerminal, Search, Wrench, X } from "lucide-react-native";
|
||||
import { extractPrincipalParam } from "@/utils/tool-call-parsers";
|
||||
import { ToolCallDetailsContent, useToolCallDetails } from "./tool-call-details";
|
||||
import { Pencil, Eye, SquareTerminal, Search, Bot, Wrench, X } from "lucide-react-native";
|
||||
import type { ToolCallDisplayInfo } from "@/utils/tool-call-parsers";
|
||||
import { ToolCallDetailsContent } from "./tool-call-details";
|
||||
|
||||
// ----- Types -----
|
||||
|
||||
export interface ToolCallSheetData {
|
||||
toolName: string;
|
||||
kind?: string;
|
||||
status?: "executing" | "completed" | "failed";
|
||||
cwd?: string;
|
||||
args?: unknown;
|
||||
result?: unknown;
|
||||
error?: unknown;
|
||||
}
|
||||
export type ToolCallSheetData = ToolCallDisplayInfo;
|
||||
|
||||
interface ToolCallSheetContextValue {
|
||||
openToolCall: (data: ToolCallSheetData) => void;
|
||||
@@ -55,6 +47,7 @@ const toolKindIcons: Record<string, React.ComponentType<{ size?: number; color?:
|
||||
read: Eye,
|
||||
execute: SquareTerminal,
|
||||
search: Search,
|
||||
agent: Bot,
|
||||
};
|
||||
|
||||
// ----- Custom Background Component -----
|
||||
@@ -140,14 +133,9 @@ interface ToolCallSheetContentProps {
|
||||
}
|
||||
|
||||
function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
||||
const { toolName, kind, cwd, args, result, error } = data;
|
||||
const { kind, displayName, detail, errorText } = data;
|
||||
|
||||
const IconComponent = kind
|
||||
? toolKindIcons[kind.toLowerCase()] || Wrench
|
||||
: Wrench;
|
||||
|
||||
const { display, errorText } = useToolCallDetails({ toolName, args, result, error });
|
||||
const principalParam = useMemo(() => extractPrincipalParam(args, cwd), [args, cwd]);
|
||||
const IconComponent = toolKindIcons[kind] || Wrench;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@@ -155,16 +143,9 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<IconComponent size={20} color={styles.headerIcon.color} />
|
||||
<View style={styles.headerTextColumn}>
|
||||
<Text style={styles.headerTitle} numberOfLines={1}>
|
||||
{display.toolName}
|
||||
</Text>
|
||||
{principalParam ? (
|
||||
<Text style={styles.headerSubtitle} numberOfLines={1}>
|
||||
{principalParam}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<Text style={styles.headerTitle} numberOfLines={1}>
|
||||
{displayName}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable onPress={onClose} style={styles.closeButton}>
|
||||
<X size={20} color={styles.closeIcon.color} />
|
||||
@@ -176,7 +157,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
||||
style={styles.content}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
>
|
||||
<ToolCallDetailsContent display={display} errorText={errorText} />
|
||||
<ToolCallDetailsContent detail={detail} errorText={errorText} />
|
||||
</BottomSheetScrollView>
|
||||
</View>
|
||||
);
|
||||
@@ -210,10 +191,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
gap: theme.spacing[2],
|
||||
flex: 1,
|
||||
},
|
||||
headerTextColumn: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
headerIcon: {
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
@@ -223,11 +200,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
color: theme.colors.foreground,
|
||||
flex: 1,
|
||||
},
|
||||
headerSubtitle: {
|
||||
marginTop: theme.spacing[1],
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
closeButton: {
|
||||
padding: theme.spacing[2],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user