Files
paseo/packages/app/src/components/tool-call-details.tsx
T
Mohamed Boudra e3fad77d5b 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
2026-02-07 21:13:24 +07:00

319 lines
9.4 KiB
TypeScript

import React, { useMemo, ReactNode } from "react";
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 {
buildLineDiff,
parseUnifiedDiff,
type ToolCallDetail,
} from "@/utils/tool-call-parsers";
import { DiffViewer } from "./diff-viewer";
import { getCodeInsets } from "./code-insets";
const ScrollView = Platform.OS === "web" ? RNScrollView : GHScrollView;
// ---- Content Component ----
interface ToolCallDetailsContentProps {
detail: ToolCallDetail;
errorText?: string;
maxHeight?: number;
}
export function ToolCallDetailsContent({
detail,
errorText,
maxHeight = 300,
}: ToolCallDetailsContentProps) {
// Compute diff lines for edit type
const diffLines = useMemo(() => {
if (detail.type !== "edit") return undefined;
// Use pre-computed unified diff if available (e.g., from apply_patch)
if (detail.unifiedDiff) {
return parseUnifiedDiff(detail.unifiedDiff);
}
return buildLineDiff(detail.oldString, detail.newString);
}, [detail]);
const sections: ReactNode[] = [];
const isFullBleed = detail.type === "edit" || detail.type === "shell";
const codeBlockStyle = isFullBleed ? styles.fullBleedBlock : styles.diffContainer;
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}>
<View style={codeBlockStyle}>
<ScrollView
style={[styles.codeVerticalScroll, { maxHeight }]}
contentContainerStyle={styles.codeVerticalContent}
nestedScrollEnabled
showsVerticalScrollIndicator
>
<ScrollView
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
contentContainerStyle={styles.codeHorizontalContent}
>
<View style={styles.codeLine}>
<Text selectable style={styles.scrollText}>
<Text style={styles.shellPrompt}>$ </Text>
{command}
{hasOutput ? `\n\n${output}` : ""}
</Text>
</View>
</ScrollView>
</ScrollView>
</View>
</View>
);
} else if (detail.type === "edit") {
sections.push(
<View key="edit" style={styles.section}>
{diffLines ? (
<View style={codeBlockStyle}>
<DiffViewer diffLines={diffLines} maxHeight={maxHeight} />
</View>
) : null}
</View>
);
} else if (detail.type === "read") {
sections.push(
<View key="read" style={styles.section}>
{(detail.offset !== undefined || detail.limit !== undefined) ? (
<Text style={styles.rangeText}>
{detail.offset !== undefined ? `Offset: ${detail.offset}` : ""}
{detail.offset !== undefined && detail.limit !== undefined ? " • " : ""}
{detail.limit !== undefined ? `Limit: ${detail.limit}` : ""}
</Text>
) : null}
{detail.content ? (
<ScrollView
style={[styles.scrollArea, { maxHeight }]}
contentContainerStyle={styles.scrollContent}
nestedScrollEnabled
showsVerticalScrollIndicator={true}
>
<ScrollView
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator={true}
>
<Text selectable style={styles.scrollText}>{detail.content}</Text>
</ScrollView>
</ScrollView>
) : null}
</View>
);
} else if (detail.type === "thinking") {
// Thinking: display the content as plain text
sections.push(
<View key="thinking" style={styles.section}>
<ScrollView
style={[styles.scrollArea, { maxHeight }]}
contentContainerStyle={styles.scrollContent}
nestedScrollEnabled
showsVerticalScrollIndicator={true}
>
<Text selectable style={styles.scrollText}>{detail.content}</Text>
</ScrollView>
</View>
);
} else {
// Generic tool: show input/output as key-value pairs
if (detail.input.length > 0) {
sections.push(
<View key="input-header" style={styles.groupHeader}>
<Text style={styles.groupHeaderText}>Input</Text>
</View>
);
detail.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 selectable style={styles.scrollText}>{pair.value}</Text>
</ScrollView>
</View>
);
});
}
if (detail.output.length > 0) {
sections.push(
<View key="output-header" style={styles.groupHeader}>
<Text style={styles.groupHeaderText}>Output</Text>
</View>
);
detail.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 selectable 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 selectable 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={isFullBleed ? styles.fullBleedContainer : styles.paddedContainer}>
{sections}
</View>
);
}
// ---- Styles ----
const styles = StyleSheet.create((theme) => {
const insets = getCodeInsets(theme);
return {
paddedContainer: {
gap: theme.spacing[4],
padding: theme.spacing[2],
},
fullBleedContainer: {
gap: theme.spacing[2],
padding: 0,
},
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.foregroundMuted,
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.semibold,
textTransform: "uppercase",
letterSpacing: 0.5,
},
rangeText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
},
diffContainer: {
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.base,
overflow: "hidden",
backgroundColor: theme.colors.surface2,
},
fullBleedBlock: {
borderWidth: 0,
borderRadius: 0,
overflow: "hidden",
backgroundColor: theme.colors.surface1,
},
codeVerticalScroll: {},
codeVerticalContent: {
flexGrow: 1,
paddingBottom: insets.extraBottom,
},
codeHorizontalContent: {
paddingRight: insets.extraRight,
},
codeLine: {
minWidth: "100%",
paddingHorizontal: insets.padding,
paddingVertical: insets.padding,
},
scrollArea: {
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.base,
backgroundColor: theme.colors.surface2,
},
scrollContent: {
padding: insets.padding,
},
scrollText: {
fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs,
color: theme.colors.foreground,
lineHeight: 18,
},
shellPrompt: {
color: theme.colors.foregroundMuted,
},
jsonScroll: {
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.base,
backgroundColor: theme.colors.surface2,
},
jsonScrollError: {
borderColor: theme.colors.destructive,
},
jsonContent: {
padding: insets.padding,
},
errorText: {
color: theme.colors.destructive,
},
emptyStateText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
fontStyle: "italic",
},
};
});