1570 lines · 50.2 KB · TypeScript
RawDownload
import * as Clipboard from "expo-clipboard";import * as Haptics from "expo-haptics";import { KeyboardAvoidingLegendList } from "@legendapp/list/keyboard";import { type LegendListRef } from "@legendapp/list/react-native";import type { ThreadId, TurnId } from "@t3tools/contracts";import { SymbolView } from "expo-symbols";import { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";import {  Markdown,  type CustomRenderers,  type NodeStyleOverrides,  type PartialMarkdownTheme,} from "react-native-nitro-markdown";import {  Image,  Linking,  type NativeScrollEvent,  type NativeSyntheticEvent,  Pressable,  ScrollView,  StyleSheet,  Text as NativeText,  type ColorValue,  useColorScheme,  useWindowDimensions,  View,} from "react-native";import { TouchableOpacity } from "react-native-gesture-handler";import ImageViewing from "react-native-image-viewing";import { useSafeAreaInsets } from "react-native-safe-area-context";import { useThemeColor } from "../../lib/useThemeColor";import {  hasNativeSelectableMarkdownText,  SelectableMarkdownText,  type NativeMarkdownTextStyle,  type SelectableMarkdownSkill,} from "../../native/SelectableMarkdownText"; import { AppText as Text } from "../../components/AppText";import { EmptyState } from "../../components/EmptyState";import { CopyTextButton } from "../../components/CopyTextButton";import {  parseReviewCommentMessageSegments,  type ReviewInlineComment,} from "../review/reviewCommentSelection";import { resolveNativeReviewDiffView } from "../diffs/nativeReviewDiffSurface";import {  buildNativeReviewDiffData,  createNativeReviewDiffTheme,  NATIVE_REVIEW_DIFF_CONTENT_WIDTH,  NATIVE_REVIEW_DIFF_ROW_HEIGHT,  NATIVE_REVIEW_DIFF_STYLE,} from "../review/nativeReviewDiffAdapter";import { buildReviewParsedDiff } from "../review/reviewModel";import { cn } from "../../lib/cn";import type { MobileLayoutVariant } from "../../lib/mobileLayout";import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons";import { resolveMarkdownLinkPresentation } from "@t3tools/mobile-markdown-text/links";import {  deriveThreadFeedPresentation,  type ThreadFeedEntry,  type ThreadFeedLatestTurn,} from "../../lib/threadActivity";import { isThreadFeedNearEnd } from "../../lib/threadFeedLayout";import { relativeTime } from "../../lib/time";import { messageImageUrl } from "./threadPresentation"; const THREAD_FEED_END_THRESHOLD = 80;const MESSAGE_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, {  hour: "numeric",  minute: "2-digit",}); function formatMessageTime(input: string): string {  const timestamp = Date.parse(input);  if (Number.isNaN(timestamp)) {    return "";  }  return MESSAGE_TIME_FORMATTER.format(timestamp);} export interface ThreadFeedProps {  readonly threadId: ThreadId;  readonly feed: ReadonlyArray<ThreadFeedEntry>;  readonly httpBaseUrl: string | null;  readonly bearerToken: string | null;  readonly agentLabel: string;  readonly latestTurn: ThreadFeedLatestTurn | null;  readonly contentTopInset?: number;  readonly contentBottomInset?: number;  readonly layoutVariant?: MobileLayoutVariant;  readonly composerExpanded?: boolean;  readonly skills?: ReadonlyArray<SelectableMarkdownSkill>;} 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 buildActivityRows(  activities: Extract<ThreadFeedEntry, { type: "activity-group" }>["activities"],) {  return activities.map((activity) => ({    ...activity,    detail: compactActivityDetail(activity.detail),  }));} const MAX_VISIBLE_WORK_LOG_ENTRIES = 1; const MARKDOWN_COLORS = {  light: {    body: "#111111",    strong: "#000000",    link: "#2563eb",    blockquoteBorder: "rgba(0, 0, 0, 0.08)",    blockquoteBackground: "rgba(0, 0, 0, 0.02)",    codeBackground: "rgba(0, 0, 0, 0.04)",    codeText: "#262626",    horizontalRule: "rgba(0, 0, 0, 0.08)",    userBody: "#ffffff",    userCodeBackground: "rgba(255, 255, 255, 0.22)",    userCodeText: "#ffffff",    userFenceBackground: "rgba(0, 0, 0, 0.16)",    userFenceText: "#ffffff",  },  dark: {    body: "#e5e5e5",    strong: "#f5f5f5",    link: "#60a5fa",    blockquoteBorder: "rgba(255, 255, 255, 0.1)",    blockquoteBackground: "rgba(255, 255, 255, 0.03)",    codeBackground: "rgba(255, 255, 255, 0.06)",    codeText: "#e5e5e5",    horizontalRule: "rgba(255, 255, 255, 0.08)",    userBody: "#ffffff",    userCodeBackground: "rgba(255, 255, 255, 0.18)",    userCodeText: "#ffffff",    userFenceBackground: "rgba(0, 0, 0, 0.28)",    userFenceText: "#ffffff",  },} as const; interface MarkdownStyleSets {  readonly user: MarkdownStyleSet;  readonly assistant: MarkdownStyleSet;} interface MarkdownStyleSet {  readonly theme: PartialMarkdownTheme;  readonly styles: NodeStyleOverrides;  readonly renderers: CustomRenderers;  readonly nativeTextStyle: NativeMarkdownTextStyle;} interface ReviewCommentColors {  readonly background: ColorValue;  readonly border: ColorValue;  readonly mutedBackground: ColorValue;  readonly text: ColorValue;  readonly mutedText: ColorValue;  readonly codeBackground: ColorValue;} const failedMarkdownFaviconHosts = new Set<string>();const markdownLinkStyles = StyleSheet.create({  favicon: {    width: 14,    height: 14,    borderRadius: 3,    marginHorizontal: 3,    transform: [{ translateY: 2 }],  },  file: {    borderRadius: 5,    borderWidth: StyleSheet.hairlineWidth,    fontFamily: "DMSans_500Medium",    fontSize: 13,    lineHeight: 20,    paddingHorizontal: 6,    paddingVertical: 2,  },  fileIcon: {    width: 15,    height: 15,    marginRight: 4,    transform: [{ translateY: 2 }],  },}); const MarkdownExternalLink = memo(function MarkdownExternalLink(props: {  readonly children: ReactNode;  readonly color: string;  readonly host: string;  readonly href: string;}) {  const [failed, setFailed] = useState(() => failedMarkdownFaviconHosts.has(props.host));   return (    <NativeText      onPress={() => {        void Linking.openURL(props.href);      }}      style={{        color: props.color,        fontFamily: "DMSans_400Regular",        textDecorationLine: "none",      }}    >      {!failed ? (        <Image          source={{            uri: `https://www.google.com/s2/favicons?domain=${encodeURIComponent(props.host)}&sz=32`,          }}          style={markdownLinkStyles.favicon}          onError={() => {            failedMarkdownFaviconHosts.add(props.host);            setFailed(true);          }}        />      ) : (        <NativeText style={{ color: props.color }}>{" ◉ "}</NativeText>      )}      {props.children}    </NativeText>  );}); function useReviewCommentColors(): ReviewCommentColors {  const colorScheme = useColorScheme();  const isDark = colorScheme === "dark";  const background = isDark ? "#151515" : "#ffffff";  const border = isDark ? "#2a2a2a" : "#d7d7d7";  const mutedBackground = isDark ? "#242424" : "#f2f2f2";  const text = isDark ? "#f3f3f3" : "#111111";  const mutedText = isDark ? "#8f8f8f" : "#666666";  const codeBackground = isDark ? "#0f0f0f" : "#ffffff";   return useMemo(    () => ({      background,      border,      mutedBackground,      text,      mutedText,      codeBackground,    }),    [background, border, codeBackground, mutedBackground, mutedText, text],  );} function useMarkdownStyles(): MarkdownStyleSets {  const colorScheme = useColorScheme();  const colors = MARKDOWN_COLORS[colorScheme === "dark" ? "dark" : "light"];  const inlineChipBackground = String(useThemeColor("--color-subtle"));  const inlineSkillBackground = String(useThemeColor("--color-inline-skill-background"));  const inlineSkillForeground = String(useThemeColor("--color-inline-skill-foreground"));   return useMemo(() => {    const markdownBodyColor = colors.body;    const markdownStrongColor = colors.strong;    const markdownLinkColor = colors.link;    const markdownBlockquoteBg = colors.blockquoteBackground;    const markdownBlockquoteBorder = colors.blockquoteBorder;    const markdownCodeBg = colors.codeBackground;    const markdownCodeText = colors.codeText;    const markdownHrColor = colors.horizontalRule;    const markdownUserBodyColor = colors.userBody;    const markdownUserCodeBg = colors.userCodeBackground;    const markdownUserCodeText = colors.userCodeText;    const markdownUserFenceBg = colors.userFenceBackground;    const markdownUserFenceText = colors.userFenceText;     const baseTheme: PartialMarkdownTheme = {      colors: {        text: markdownBodyColor,        heading: markdownStrongColor,        link: markdownLinkColor,        blockquote: markdownBlockquoteBorder,        border: markdownHrColor,        surfaceLight: markdownBlockquoteBg,        accent: markdownLinkColor,        tableBorder: markdownHrColor,        tableHeader: markdownBlockquoteBg,        tableHeaderText: markdownStrongColor,        tableRowOdd: "transparent",        tableRowEven: "transparent",      },      spacing: {        xs: 4,        s: 4,        m: 8,        l: 8,        xl: 16,      },      fontSizes: {        s: 13,        m: 15,        h1: 20,        h2: 18,        h3: 16,        h4: 14,        h5: 14,        h6: 14,      },      fontFamilies: {        regular: "DMSans_400Regular",        heading: "DMSans_700Bold",        mono: "ui-monospace",      },      headingWeight: "700",      borderRadius: {        s: 4,        m: 8,        l: 12,      },      showCodeLanguage: false,    };     const baseStyles: NodeStyleOverrides = {      document: { flexShrink: 1 },      paragraph: { marginTop: 0, marginBottom: 10 },      list: { marginTop: 4, marginBottom: 8 },      list_item: { marginTop: 0, marginBottom: 4 },      task_list_item: { marginTop: 0, marginBottom: 4 },      text: { lineHeight: 22 },      bold: {        fontWeight: "700",        color: markdownStrongColor,        fontFamily: "DMSans_700Bold",      },      italic: { fontStyle: "italic" },      link: {        color: markdownLinkColor,        textDecorationLine: "underline" as const,      },      blockquote: {        borderLeftWidth: 2,        borderLeftColor: markdownBlockquoteBorder,        paddingLeft: 11,        paddingVertical: 2,        marginLeft: 0,        marginVertical: 10,      },      heading: {        fontFamily: "DMSans_700Bold",        color: markdownStrongColor,        marginTop: 18,        marginBottom: 8,      },      horizontal_rule: {        backgroundColor: markdownHrColor,        height: 1,        marginVertical: 12,      },    };     const createMarkdownRenderers = (      inlineBackgroundColor: string,      inlineTextColor: string,      blockBackgroundColor: string,      blockTextColor: string,    ): CustomRenderers => ({      link: ({ children, href = "" }) => {        const presentation = resolveMarkdownLinkPresentation(href);        if (presentation.kind === "file") {          return (            <NativeText              style={[                markdownLinkStyles.file,                {                  backgroundColor: inlineBackgroundColor,                  borderColor: markdownHrColor,                  color: inlineTextColor,                },              ]}            >              <Image                source={markdownFileIconSource(presentation.icon)}                style={markdownLinkStyles.fileIcon}              />              {presentation.label}            </NativeText>          );        }        if (presentation.kind === "external") {          return (            <MarkdownExternalLink              href={presentation.href}              host={presentation.host}              color={markdownLinkColor}            >              {children}            </MarkdownExternalLink>          );        }        const linkHref = presentation.href;        return (          <NativeText            onPress={              linkHref                ? () => {                    void Linking.openURL(linkHref);                  }                : undefined            }            style={{              color: markdownLinkColor,              textDecorationLine: "underline",            }}          >            {children}          </NativeText>        );      },      list: ({ node, Renderer, ordered = false, start = 1 }) => (        <View style={{ marginTop: 2, marginBottom: 8 }}>          {node.children?.map((child, index) => {            const childKey = `${child.type}:${child.beg ?? "unknown"}:${child.end ?? "unknown"}`;            if (child.type === "task_list_item") {              return (                <Renderer key={childKey} node={child} depth={1} inListItem parentIsText={false} />              );            }            return (              <View                key={childKey}                style={{                  flexDirection: "row",                  alignItems: "flex-start",                  marginBottom: 3,                }}              >                <NativeText                  style={{                    width: ordered ? 22 : 12,                    marginRight: 5,                    color: inlineTextColor,                    fontFamily: "DMSans_400Regular",                    fontSize: 15,                    lineHeight: 22,                    textAlign: ordered ? "right" : "center",                  }}                >                  {ordered ? `${start + index}.` : "•"}                </NativeText>                <View style={{ flex: 1, minWidth: 0 }}>                  <Renderer node={child} depth={1} inListItem parentIsText={false} />                </View>              </View>            );          })}        </View>      ),      code_inline: ({ content }) => {        const value = content ?? "";        const wrapsPoorly =          value.length > 24 || value.includes("/") || value.includes("\\") || value.includes(":");        return (          <NativeText            style={{              color: inlineTextColor,              fontFamily: "ui-monospace",              fontSize: 12,              lineHeight: 18,              ...(wrapsPoorly                ? { opacity: 0.82 }                : {                    backgroundColor: inlineBackgroundColor,                    borderRadius: 4,                    paddingHorizontal: 3,                  }),            }}          >            {value}          </NativeText>        );      },      code_block: ({ content, language }) => (        <View          style={{            backgroundColor: blockBackgroundColor,            borderRadius: 10,            borderWidth: 1,            borderColor: markdownHrColor,            marginVertical: 12,            overflow: "hidden",          }}        >

Lines 1–500 of 1570.