import * as Haptics from "expo-haptics"; import { SymbolView, type SFSymbol } from "expo-symbols"; import { LayoutAnimation, Pressable, ScrollView, useColorScheme, View } from "react-native"; import { AppText as Text } from "../../components/AppText"; import { cn } from "../../lib/cn"; import type { ThreadFeedActivity } from "../../lib/threadActivity"; import Animated, { FadeIn } from "react-native-reanimated"; const WORK_LOG_LAYOUT_ANIMATION = { duration: 180, create: { type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.opacity, }, update: { type: LayoutAnimation.Types.easeInEaseOut }, delete: { type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.opacity, }, } as const; function triggerDisclosureFeedback() { LayoutAnimation.configureNext(WORK_LOG_LAYOUT_ANIMATION); void Haptics.selectionAsync(); } function stripShellWrapper(value: string): string { const trimmed = value.trim(); const match = trimmed.match(/^\/bin\/zsh -lc ['"]?([\s\S]*?)['"]?$/); return (match?.[1] ?? trimmed).trim(); } function compactActivityDetail(detail: string | null): string | null { if (!detail) { return null; } const cleaned = stripShellWrapper(detail).replace(/\s+/g, " ").trim(); return cleaned.length > 0 ? cleaned : null; } function workRowSymbolName(icon: ThreadFeedActivity["icon"]): SFSymbol { switch (icon) { case "agent": return "sparkles"; case "alert": return "exclamationmark.triangle"; case "check": return "checkmark"; case "command": return "terminal"; case "edit": return "square.and.pencil"; case "eye": return "eye"; case "globe": return "globe"; case "hammer": return "hammer"; case "message": return "bubble.left"; case "warning": return "xmark"; case "wrench": return "wrench"; case "zap": return "bolt"; } } // Entering fades only for rows created moments ago: rows remount whenever the // list scrolls them back into view, and old rows must not replay an entrance. const FRESH_ROW_WINDOW_MS = 3_000; function isFreshRow(createdAt: string): boolean { const timestamp = Date.parse(createdAt); return Number.isFinite(timestamp) && Date.now() - timestamp < FRESH_ROW_WINDOW_MS; } export function ThreadWorkLog(props: { readonly activities: ReadonlyArray; readonly copiedRowId: string | null; readonly expandedRows: Readonly>; readonly iconSubtleColor: import("react-native").ColorValue; readonly onCopyRow: (rowId: string, value: string) => void; readonly onToggleRow: (rowId: string) => void; }) { const colorScheme = useColorScheme(); const pressedBackground = colorScheme === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.035)"; const rows = props.activities .filter((activity) => !(activity.toolLike && activity.status === "neutral")) .map((activity) => ({ ...activity, detail: compactActivityDetail(activity.detail) })); if (rows.length === 0) { return null; } const onlyToolRows = rows.every((row) => row.toolLike); return ( {!onlyToolRows ? ( work log ) : null} {rows.map((row) => { const expanded = props.expandedRows[row.id] ?? false; const canExpand = row.fullDetail !== null; const displayText = row.detail ? `${row.summary} ${row.detail}` : row.summary; const iconIsDestructive = row.icon === "alert" || row.icon === "warning"; return ( { if (canExpand) { triggerDisclosureFeedback(); props.onToggleRow(row.id); } }} onLongPress={() => props.onCopyRow(row.id, row.copyText)} style={({ pressed }) => ({ backgroundColor: pressed ? pressedBackground : "transparent", })} className="rounded-md px-0.5 py-0" > {row.summary} {row.detail ? ( {row.detail} ) : null} {props.copiedRowId === row.id ? ( Copied ) : null} {canExpand ? ( ) : null} {row.status ? ( ) : null} {expanded && row.fullDetail ? ( {row.fullDetail} ) : null} ); })} ); } export function ThreadWorkGroupToggle(props: { readonly expanded: boolean; readonly hiddenCount: number; readonly iconSubtleColor: import("react-native").ColorValue; readonly onlyToolActivities: boolean; readonly onToggle: () => void; }) { const colorScheme = useColorScheme(); const pressedBackground = colorScheme === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.035)"; const noun = props.onlyToolActivities ? props.hiddenCount === 1 ? "tool call" : "tool calls" : props.hiddenCount === 1 ? "log entry" : "log entries"; const collapsedLabel = `Show ${props.hiddenCount} previous ${noun}`; const expandedLabel = props.onlyToolActivities ? "Show fewer tool calls" : "Show fewer log entries"; return ( { void Haptics.selectionAsync(); props.onToggle(); }} style={({ pressed }) => ({ backgroundColor: pressed ? pressedBackground : "transparent", })} className="min-h-8 flex-row items-center gap-1.5 rounded-md px-0.5 py-0" > {props.expanded ? expandedLabel : `+${props.hiddenCount} previous ${noun}`} ); }