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 type { ToolCallDetail } from "@server/server/agent/agent-sdk-types"; import { buildLineDiff, parseUnifiedDiff } from "@/utils/tool-call-parsers"; import { hasMeaningfulToolCallDetail } from "@/utils/tool-call-detail-state"; 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; fillAvailableHeight?: boolean; showLoadingSkeleton?: boolean; } export function ToolCallDetailsContent({ detail, errorText, maxHeight, fillAvailableHeight = false, showLoadingSkeleton = false, }: ToolCallDetailsContentProps) { const resolvedMaxHeight = fillAvailableHeight ? undefined : (maxHeight ?? 300); // Compute diff lines for edit type const diffLines = useMemo(() => { if (!detail || 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" || detail?.type === "write"; const shouldFill = fillAvailableHeight && (detail?.type === "shell" || detail?.type === "edit" || detail?.type === "write" || detail?.type === "read" || detail?.type === "sub_agent"); const codeBlockStyle = isFullBleed ? styles.fullBleedBlock : styles.diffContainer; if (detail?.type === "shell") { const command = detail.command.replace(/\n+$/, ""); const commandOutput = (detail.output ?? "").replace(/^\n+/, ""); const hasOutput = commandOutput.length > 0; sections.push( $ {command} {hasOutput ? `\n\n${commandOutput}` : ""} , ); } else if (detail?.type === "worktree_setup") { const setupLog = detail.log.replace(/^\n+/, ""); const hasLog = setupLog.length > 0; sections.push( {hasLog ? setupLog : `Preparing worktree ${detail.branchName} at ${detail.worktreePath}`} , ); } else if (detail?.type === "sub_agent") { const activityLog = detail.log.replace(/^\n+/, ""); const hasLog = activityLog.length > 0; const fallbackHeader = detail.subAgentType && detail.description ? `${detail.subAgentType}: ${detail.description}` : (detail.subAgentType ?? detail.description ?? "Sub-agent activity"); sections.push( {hasLog ? activityLog : fallbackHeader} , ); } else if (detail?.type === "edit") { sections.push( {diffLines ? ( ) : null} , ); } else if (detail?.type === "write") { sections.push( {detail.content ? ( {detail.content} ) : null} , ); } else if (detail?.type === "read") { if (detail.content) { sections.push( {detail.content} , ); } } else if (detail?.type === "search") { const searchSections: ReactNode[] = []; if (detail.query) { searchSections.push( {detail.query} , ); } if (detail.content) { searchSections.push( {detail.content} , ); } if (detail.filePaths && detail.filePaths.length > 0) { searchSections.push( {detail.filePaths.join("\n")} , ); } if (detail.webResults && detail.webResults.length > 0) { searchSections.push( {detail.webResults.map((entry) => `${entry.title}\n${entry.url}`).join("\n\n")} , ); } if (detail.annotations && detail.annotations.length > 0) { searchSections.push( {detail.annotations.join("\n\n")} , ); } sections.push(...searchSections); } else if (detail?.type === "fetch") { sections.push( {detail.result ? `${detail.url}\n\n${detail.result}` : detail.url} , ); } else if (detail?.type === "plain_text") { if (detail.text) { sections.push( {detail.text} , ); } } else if (detail?.type === "unknown") { const plainInputText = typeof detail.input === "string" && detail.output === null ? detail.input : null; if (plainInputText !== null) { sections.push( {plainInputText} , ); } else { const sectionsFromTopLevel = [ { title: "Input", value: detail.input }, { title: "Output", value: detail.output }, ].filter((entry) => hasMeaningfulToolCallDetail({ type: "unknown", input: entry.value ?? null, output: null, }), ); for (const section of sectionsFromTopLevel) { let value = ""; try { value = typeof section.value === "string" ? section.value : JSON.stringify(section.value, null, 2); } catch { value = String(section.value); } if (!value.length) { continue; } sections.push( {section.title} , ); sections.push( {value} , ); } } } // Always show errors if available if (errorText) { sections.push( Error {errorText} , ); } if (sections.length === 0) { if (showLoadingSkeleton) { return ( ); } return No additional details available; } return ( {sections} ); } // ---- Styles ---- const styles = StyleSheet.create((theme) => { const insets = getCodeInsets(theme); return { paddedContainer: { gap: theme.spacing[4], padding: 0, }, fullBleedContainer: { gap: theme.spacing[2], padding: 0, }, groupHeader: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], borderBottomWidth: theme.borderWidth[1], borderBottomColor: theme.colors.border, }, groupHeaderText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, }, section: { gap: theme.spacing[2], }, fillHeight: { flex: 1, minHeight: 0, }, plainTextSection: { gap: theme.spacing[2], padding: theme.spacing[3], }, plainText: { fontFamily: Fonts.sans, fontSize: theme.fontSize.base, color: theme.colors.foreground, lineHeight: 22, overflowWrap: "anywhere", }, 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, ...(Platform.OS === "web" ? { whiteSpace: "pre", overflowWrap: "normal", } : null), }, 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", }, loadingContainer: { gap: theme.spacing[2], padding: theme.spacing[3], }, loadingLineWide: { height: 12, width: "100%", borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.surface3, }, loadingLineMedium: { height: 12, width: "72%", borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.surface3, }, loadingLineShort: { height: 12, width: "48%", borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.surface3, }, }; });