1837 lines · 63.2 KB · TypeScript
RawDownload
import * as Haptics from "expo-haptics";import { KeyboardAwareLegendList } from "@legendapp/list/keyboard";import { type LegendListRef } from "@legendapp/list/react-native";import type { EnvironmentId, MessageId, ThreadId, TurnId } from "@t3tools/contracts";import { CHAT_LIST_ANCHOR_OFFSET, resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList";import { formatElapsed } from "@t3tools/shared/orchestrationTiming";import { SymbolView } from "../../components/AppSymbol";import { HeaderHeightContext } from "@react-navigation/elements";import { useNavigation } from "@react-navigation/native";import {  memo,  useCallback,  useContext,  useEffect,  useLayoutEffect,  useMemo,  useRef,  useState,  type ReactNode,  type RefObject,} from "react";import {  Markdown,  type CustomRenderers,  type NodeStyleOverrides,  type PartialMarkdownTheme,} from "react-native-nitro-markdown";import {  ActivityIndicator,  Image,  Linking,  Platform,  type LayoutChangeEvent,  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 Animated, {  FadeIn,  FadeInUp,  LinearTransition,  type SharedValue,} from "react-native-reanimated";import { useThemeColor } from "../../lib/useThemeColor";import { useFontFamily } from "../../lib/useFontFamily";import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";import {  hasNativeSelectableMarkdownText,  SelectableMarkdownText,  type NativeMarkdownTextStyle,  type SelectableMarkdownSkill,} from "../../native/SelectableMarkdownText"; import { AppText as Text } from "../../components/AppText";import { CopyTextButton } from "../../components/CopyTextButton";import {  parseReviewCommentMessageSegments,  type ReviewInlineComment,} from "../review/reviewCommentSelection";import type { ReviewDiffTheme } from "../review/shikiReviewHighlighter";import { resolveNativeReviewDiffView } from "../diffs/nativeReviewDiffSurface";import {  buildNativeReviewDiffData,  createNativeReviewDiffTheme,  NATIVE_REVIEW_DIFF_CONTENT_WIDTH,} from "../review/nativeReviewDiffAdapter";import { buildReviewParsedDiff } from "../review/reviewModel";import { cn } from "../../lib/cn";import { deriveCenteredContentHorizontalPadding, type LayoutVariant } from "../../lib/layout";import {  resolveMarkdownFontSizes,  resolveNativeMarkdownTypography,} from "../../lib/appearancePreferences";import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface";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 type { ThreadContentPresentation } from "./threadContentPresentation";import { ThreadWorkGroupToggle, ThreadWorkLog } from "./thread-work-log";import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState";import { useAssetUrl } from "../../state/assets";import { resolveWorkspaceRelativeFilePath } from "../files/filePath"; 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);} // Rows shift when content above them grows (streaming text, work-log folds);// animating the container position turns those jumps into slides.const FEED_ITEM_LAYOUT_TRANSITION = LinearTransition.duration(180); // Entering animations must only play for rows born just now — LegendList// remounts rows when they scroll back into view, and replaying an entrance for// old content would be its own kind of jank.const FRESH_ENTRY_WINDOW_MS = 3_000;function isFreshTimestamp(input: string): boolean {  const timestamp = Date.parse(input);  return Number.isFinite(timestamp) && Date.now() - timestamp < FRESH_ENTRY_WINDOW_MS;} export interface ThreadFeedProps {  readonly environmentId: EnvironmentId;  readonly threadId: ThreadId;  readonly workspaceRoot?: string | null;  readonly feed: ReadonlyArray<ThreadFeedEntry>;  readonly contentPresentation: ThreadContentPresentation;  readonly agentLabel: string;  readonly latestTurn: ThreadFeedLatestTurn | null;  readonly activeWorkStartedAt: string | null;  readonly listRef: RefObject<LegendListRef | null>;  readonly freeze: SharedValue<boolean>;  readonly anchorMessageId: MessageId | null;  readonly contentInsetEndAdjustment: SharedValue<number>;  readonly contentTopInset?: number;  readonly contentBottomInset?: number;  readonly contentMaxWidth?: number;  readonly layoutVariant?: LayoutVariant;  readonly usesAutomaticContentInsets?: boolean;  readonly onHeaderMaterialVisibilityChange?: (visible: boolean) => void;  readonly skills?: ReadonlyArray<SelectableMarkdownSkill>;} function MessageAttachmentImage(props: {  readonly environmentId: EnvironmentId;  readonly attachmentId: string;  readonly className: string;  readonly onPressImage: (uri: string, headers?: Record<string, string>) => void;}) {  const uri = useAssetUrl(props.environmentId, {    _tag: "attachment",    attachmentId: props.attachmentId,  });   if (uri === null) {    return (      <View className={`${props.className} items-center justify-center`}>        <ActivityIndicator />      </View>    );  }   return (    <TouchableOpacity activeOpacity={0.7} onPress={() => props.onPressImage(uri)}>      <Image source={{ uri }} className={props.className} resizeMode="cover" />    </TouchableOpacity>  );} 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",    inlineCodeText: "#5f6368",    horizontalRule: "rgba(0, 0, 0, 0.08)",    userBody: "#ffffff",    userCodeBackground: "rgba(255, 255, 255, 0.22)",    userCodeText: "#ffffff",    userInlineCodeText: "rgba(255, 255, 255, 0.82)",    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",    inlineCodeText: "#b8bcc2",    horizontalRule: "rgba(255, 255, 255, 0.08)",    userBody: "#ffffff",    userCodeBackground: "rgba(255, 255, 255, 0.18)",    userCodeText: "#ffffff",    userInlineCodeText: "rgba(255, 255, 255, 0.82)",    userFenceBackground: "rgba(0, 0, 0, 0.28)",    userFenceText: "#ffffff",  },} as const; const MARKDOWN_MONO_FONT = Platform.select({  ios: "ui-monospace",  android: "monospace",  default: "monospace",}); 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({  inlineIcon: {    width: 14,    height: 14,    marginHorizontal: 3,    transform: [{ translateY: 2 }],  },  favicon: {    borderRadius: 3,  },}); 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      className="font-sans"      onPress={() => {        void Linking.openURL(props.href);      }}      style={{        color: props.color,        textDecorationLine: "none",      }}    >      {!failed ? (        <Image          source={{            uri: `https://www.google.com/s2/favicons?domain=${encodeURIComponent(props.host)}&sz=32`,          }}          style={[markdownLinkStyles.inlineIcon, markdownLinkStyles.favicon]}          onError={() => {            failedMarkdownFaviconHosts.add(props.host);            setFailed(true);          }}        />      ) : (        <NativeText style={{ color: props.color }}>{" ◉ "}</NativeText>      )}      {props.children}    </NativeText>  );}); function MarkdownCodeBlock(props: {  readonly backgroundColor: string;  readonly borderColor: string;  readonly content: string;  readonly copyTintColor: ColorValue;  readonly headerTextColor: string;  readonly fontSize: number;  readonly highlightCode: boolean;  readonly language?: string | null;  readonly lineHeight: number;  readonly textColor: string;  readonly theme: ReviewDiffTheme;}) {  const content = props.content.replace(/\n$/, "");  const languageLabel = props.language?.trim() || "text";  const highlighted = useMarkdownCodeHighlight({    code: content,    enabled: props.highlightCode && Boolean(props.language?.trim()),    language: props.language,    theme: props.theme,  });  let tokenOffset = 0;   return (    <View      className="my-3 min-w-0 max-w-full self-stretch overflow-hidden rounded-lg border"      style={{ backgroundColor: props.backgroundColor, borderColor: props.borderColor }}    >      <View        className="flex-row items-center justify-between gap-2 border-b py-1 pr-1.5 pl-3.5"        style={{ borderBottomColor: props.borderColor }}      >        <NativeText          className="flex-1 font-mono uppercase opacity-70"          numberOfLines={1}          style={{            color: props.headerTextColor,            fontSize: props.fontSize,            ...(Platform.OS === "android" ? { includeFontPadding: false } : null),          }}        >          {languageLabel}        </NativeText>        <CopyTextButton          accessibilityLabel="Copy code"          text={content}          tintColor={props.copyTintColor}          buttonSize={32}          iconSize={16}        />      </View>      <ScrollView        horizontal        bounces={false}        nestedScrollEnabled={Platform.OS === "android"}        showsHorizontalScrollIndicator={false}        contentContainerClassName="px-3.5 py-3"      >        <NativeText          selectable          className="font-mono"          style={{            color: props.textColor,            fontSize: props.fontSize,            lineHeight: props.lineHeight,            ...(Platform.OS === "android" ? { includeFontPadding: false } : null),          }}        >          {highlighted            ? highlighted.map((line, lineIndex) => {                const lineStartOffset = tokenOffset;                const lineText = line.map((token) => token.content).join("");                const renderedLine = (                  <NativeText key={`line:${lineStartOffset}:${lineText}`}>                    {line.map((token) => {                      const startOffset = tokenOffset;                      tokenOffset += token.content.length;                      const fontStyle =                        token.fontStyle !== null && (token.fontStyle & 1) === 1                          ? ("italic" as const)                          : ("normal" as const);                      const fontWeight =                        token.fontStyle !== null && (token.fontStyle & 2) === 2                          ? ("700" as const)                          : ("400" as const);                       return (                        <NativeText                          key={`${startOffset}:${token.content}:${token.color ?? ""}:${                            token.fontStyle ?? ""                          }`}                          style={{                            color: token.color ?? props.textColor,                            fontStyle,                            fontWeight,                          }}                        >                          {token.content}                        </NativeText>                      );                    })}                    {lineIndex + 1 < highlighted.length ? "\n" : ""}                  </NativeText>                );                if (lineIndex + 1 < highlighted.length) {                  tokenOffset += 1;                }                return renderedLine;              })            : content}        </NativeText>      </ScrollView>    </View>  );} 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(onLinkPress: (href: string) => void): MarkdownStyleSets {  const colorScheme = useColorScheme();  const { appearance } = useAppearancePreferences();  const markdownFontSizes = useMemo(    () => resolveMarkdownFontSizes(appearance.baseFontSize),    [appearance.baseFontSize],  );  const nativeMarkdownTypography = useMemo(    () => resolveNativeMarkdownTypography(appearance.baseFontSize),    [appearance.baseFontSize],  );  const themeMode = colorScheme === "dark" ? "dark" : "light";  const colors = MARKDOWN_COLORS[themeMode];  const iconSubtleColor = String(useThemeColor("--color-icon-subtle"));  const inlineSkillForeground = String(useThemeColor("--color-inline-skill-foreground"));  const userBubbleForegroundMuted = String(useThemeColor("--color-user-bubble-foreground-muted"));  const regularFontFamily = useFontFamily("regular");  const boldFontFamily = useFontFamily("bold");   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 markdownInlineCodeText = colors.inlineCodeText;    const markdownHrColor = colors.horizontalRule;    const markdownUserBodyColor = colors.userBody;    const markdownUserCodeBg = colors.userCodeBackground;    const markdownUserCodeText = colors.userCodeText;    const markdownUserInlineCodeText = colors.userInlineCodeText;    const markdownUserFenceBg = colors.userFenceBackground;    const markdownUserFenceText = colors.userFenceText;     const baseTheme: PartialMarkdownTheme = {      colors: {        text: markdownBodyColor,        heading: markdownStrongColor,        link: markdownLinkColor,        blockquote: markdownBlockquoteBorder,        border: markdownHrColor,        surface: "transparent",        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: markdownFontSizes.s,        m: markdownFontSizes.m,        h1: markdownFontSizes.h1,        h2: markdownFontSizes.h2,        h3: markdownFontSizes.h3,        h4: markdownFontSizes.h4,        h5: markdownFontSizes.h5,        h6: markdownFontSizes.h6,      },      fontFamilies: {        regular: regularFontFamily,        heading: boldFontFamily,        mono: MARKDOWN_MONO_FONT,      },      headingWeight: "700",

Lines 1–500 of 1837.