261 lines · 9.3 KB · TypeScript
RawDownload
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"; const MAX_VISIBLE_WORK_LOG_ENTRIES = 1;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";  }} export function ThreadWorkLog(props: {  readonly activities: ReadonlyArray<ThreadFeedActivity>;  readonly copiedRowId: string | null;  readonly expanded: boolean;  readonly expandedRows: Readonly<Record<string, boolean>>;  readonly iconSubtleColor: import("react-native").ColorValue;  readonly onCopyRow: (rowId: string, value: string) => void;  readonly onToggleGroup: () => 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 hasOverflow = rows.length > MAX_VISIBLE_WORK_LOG_ENTRIES;  const visibleRows =    hasOverflow && !props.expanded ? rows.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES) : rows;  const hiddenCount = rows.length - visibleRows.length;  const onlyToolRows = rows.every((row) => row.toolLike);   return (    <View className="-mx-1 mb-3 px-1 py-0.5">      {!onlyToolRows ? (        <Text className="px-0.5 pb-0.5 font-t3-medium text-[11px] text-foreground-muted opacity-60">          work log        </Text>      ) : null}       <View className="gap-px">        {visibleRows.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 (            <View key={row.id}>              <Pressable                accessibilityRole={canExpand ? "button" : undefined}                accessibilityLabel={displayText}                accessibilityHint={                  canExpand                    ? "Double tap to show full details. Long press to copy."                    : "Long press to copy."                }                accessibilityState={canExpand ? { expanded } : undefined}                hitSlop={4}                onPress={() => {                  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.5"              >                <View className="min-h-9 flex-row items-center gap-1.5">                  <View className="h-5 w-5 shrink-0 items-center justify-center">                    <SymbolView                      name={workRowSymbolName(row.icon)}                      size={14}                      weight="medium"                      tintColor={iconIsDestructive ? "#e11d48" : props.iconSubtleColor}                      type="monochrome"                    />                  </View>                   <Text                    className="min-w-0 flex-1 text-xs leading-5 text-foreground"                    numberOfLines={1}                  >                    <Text                      className={cn(                        "font-t3-medium text-foreground",                        iconIsDestructive && "text-rose-600 dark:text-rose-400",                      )}                    >                      {row.summary}                    </Text>                    {row.detail ? (                      <Text className="text-foreground-muted opacity-60"> {row.detail}</Text>                    ) : null}                  </Text>                   <View className="shrink-0 flex-row items-center gap-px">                    {props.copiedRowId === row.id ? (                      <Text className="pr-1 font-t3-medium text-[10px] text-emerald-600 dark:text-emerald-400">                        Copied                      </Text>                    ) : null}                    <View className="h-4 w-4 items-center justify-center">                      {canExpand ? (                        <SymbolView                          name={expanded ? "chevron.up" : "chevron.down"}                          size={11}                          tintColor={props.iconSubtleColor}                          type="monochrome"                        />                      ) : null}                    </View>                    <View className="h-4 w-4 items-center justify-center">                      {row.status ? (                        <SymbolView                          name={                            row.status === "failure"                              ? "xmark"                              : row.status === "success"                                ? "checkmark"                                : "minus"                          }                          size={11}                          tintColor={row.status === "failure" ? "#e11d48" : props.iconSubtleColor}                          type="monochrome"                        />                      ) : null}                    </View>                  </View>                </View>              </Pressable>               {expanded && row.fullDetail ? (                <View className="ml-7 border-l border-neutral-300/60 pb-1.5 pl-3 pt-0.5 dark:border-white/[0.12]">                  <ScrollView                    nestedScrollEnabled                    directionalLockEnabled                    showsVerticalScrollIndicator                    style={{ maxHeight: 240 }}                    contentContainerStyle={{ paddingRight: 8 }}                  >                    <Text                      selectable                      className="text-[11px] leading-[17px] text-foreground-muted"                      style={{ fontFamily: "ui-monospace" }}                    >                      {row.fullDetail}                    </Text>                  </ScrollView>                </View>              ) : null}            </View>          );        })}      </View>       {hasOverflow ? (        <Pressable          accessibilityRole="button"          accessibilityState={{ expanded: props.expanded }}          accessibilityLabel={            props.expanded              ? "Show fewer tool calls"              : `Show ${hiddenCount} previous tool ${hiddenCount === 1 ? "call" : "calls"}`          }          hitSlop={4}          onPress={() => {            triggerDisclosureFeedback();            props.onToggleGroup();          }}          style={({ pressed }) => ({            backgroundColor: pressed ? pressedBackground : "transparent",          })}          className="min-h-9 flex-row items-center gap-1.5 rounded-md px-0.5 py-0.5"        >          <View className="h-5 w-5 items-center justify-center">            <SymbolView              name={props.expanded ? "chevron.up" : "chevron.down"}              size={13}              tintColor={props.iconSubtleColor}              type="monochrome"            />          </View>          <Text className="font-t3-medium text-xs text-foreground opacity-80">            {props.expanded              ? "Show fewer tool calls"              : `+${hiddenCount} previous tool ${hiddenCount === 1 ? "call" : "calls"}`}          </Text>        </Pressable>      ) : null}    </View>  );}