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 { classifyMarkdownImageSource } from "@t3tools/client-runtime/markdown-images";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, Platform, type LayoutChangeEvent, type NativeScrollEvent, type NativeSyntheticEvent, Pressable, ScrollView, StyleSheet, Text as NativeText, type ColorValue, useWindowDimensions, View, type ViewStyle,} 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, type SharedValue } from "react-native-reanimated";import { useThemeColor } from "../../lib/useThemeColor";import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";import { useFontFamily } from "../../lib/useFontFamily";import { scopedThreadKey } from "../../lib/scopedEntities";import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";import { tryOpenExternalUrl } from "../../lib/openExternalUrl";import { hasWideMarkdownBlock } from "../../lib/wideMarkdownBlocks";import { hasNativeSelectableMarkdownText, SelectableMarkdownText, type MarkdownImageRenderer, 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, deriveThreadFeedInitialContentInset, type LayoutVariant,} from "../../lib/layout";import { resolveMarkdownFontSizes, resolveNativeMarkdownTypography, scaledTypographyLineHeight,} from "../../lib/appearancePreferences";import { MOBILE_TYPOGRAPHY } from "../../lib/typography";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 { resolveThreadFeedLiveFollow, type ThreadFeedLiveFollowEvent,} from "./thread-feed-live-follow";import { collapsedWorkLogHeight, ThreadWorkGroupToggle, ThreadWorkLog, WORK_GROUP_TOGGLE_HEIGHT,} from "./thread-work-log";import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState";import { useAssetUrl, useAssetUrlState } from "../../state/assets";import { resolveWorkspaceRelativeFilePath } from "../files/filePath";import { MARKDOWN_IMAGE_MAX_WIDTH, resolveMarkdownImageDisplaySize } from "./markdownImageSize"; const WIDE_MARKDOWN_BLOCK_OPTIONS = { includeOrderedLists: Platform.OS === "android",} as const; 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);} // Pre-measurement heights for getFixedItemSize, mirroring renderFeedEntry's// classNames. The fold row's min-h-11 (44px) stays taller than its single// text-sm line at every supported base font size (26px at the 22pt maximum),// so its height is a constant; a drifted value costs one correction on// measure, not a persistent offset.const TURN_FOLD_HEIGHT = 56; // min-h-11 (44) + mb-3 (12)// The working row has no min-height clamp — its height follows the scaled// text-xs line height (see workingRowHeight in ThreadFeed).const WORKING_ROW_VERTICAL_EXTRAS = 24; // py-1 (8) + mb-4 (16) // 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 submittedMessageId: 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 onEndFollowEnabledChange?: (enabled: boolean) => void; readonly skills?: ReadonlyArray<SelectableMarkdownSkill>; /** Non-null when older turns exist beyond the loaded window. */ readonly loadEarlier?: { readonly loading: boolean; readonly onLoadEarlier: () => void; } | null;} 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> );} function ThreadMarkdownImageView(props: { readonly uri: string | null; readonly sourceKey: string; readonly unavailable: boolean; readonly alt: string | null; readonly onPressImage: (uri: string) => void;}) { const codeBackground = useThemeColor("--color-md-code-bg"); const [availableWidth, setAvailableWidth] = useState(0); const [sourceSize, setSourceSize] = useState<{ width: number; height: number } | null>(null); const [failedUri, setFailedUri] = useState<string | null>(null); useEffect(() => { setSourceSize(null); }, [props.sourceKey]); useEffect(() => { setFailedUri(null); }, [props.uri]); const displaySize = sourceSize === null ? null : resolveMarkdownImageDisplaySize({ sourceWidth: sourceSize.width, sourceHeight: sourceSize.height, availableWidth, }); const failed = props.unavailable || (props.uri !== null && failedUri === props.uri); const placeholderWidth: ViewStyle["width"] = availableWidth > 0 ? Math.min(availableWidth, MARKDOWN_IMAGE_MAX_WIDTH) : "100%"; const frameStyle: ViewStyle = displaySize ?? { width: placeholderWidth, aspectRatio: 16 / 9 }; return ( <View onLayout={(event) => setAvailableWidth(event.nativeEvent.layout.width)} style={{ alignSelf: "stretch", gap: 6 }} > {props.uri === null || failed ? ( <View style={{ ...frameStyle, borderRadius: 10, backgroundColor: codeBackground, alignItems: "center", justifyContent: "center", }} > {failed ? ( <Text className="text-xs text-foreground-muted">Image unavailable</Text> ) : ( <ActivityIndicator /> )} </View> ) : ( <TouchableOpacity accessibilityRole="imagebutton" accessibilityLabel={props.alt ?? "Markdown image"} activeOpacity={0.7} onPress={() => props.onPressImage(props.uri!)} style={{ alignSelf: "flex-start" }} > <View style={{ ...frameStyle, borderRadius: 10, backgroundColor: codeBackground, alignItems: "center", justifyContent: "center", overflow: "hidden", }} > <ThreadMarkdownImageRequest key={props.uri} uri={props.uri} onLoad={setSourceSize} onError={() => setFailedUri(props.uri)} /> </View> </TouchableOpacity> )} {props.alt ? ( <Text selectable className="text-xs text-foreground-muted"> {props.alt} </Text> ) : null} </View> );} function ThreadMarkdownImageRequest(props: { readonly uri: string; readonly onLoad: (sourceSize: { width: number; height: number }) => void; readonly onError: () => void;}) { const [loaded, setLoaded] = useState(false); return ( <> <Image source={{ uri: props.uri }} resizeMode="contain" accessible={false} onLoad={(event) => { setLoaded(true); props.onLoad(event.nativeEvent.source); }} onError={props.onError} style={{ width: "100%", height: "100%", opacity: loaded ? 1 : 0 }} /> {loaded ? null : ( <View pointerEvents="none" style={[StyleSheet.absoluteFill, { alignItems: "center", justifyContent: "center" }]} > <Text className="text-xs text-foreground-muted">Loading image…</Text> </View> )} </> );} /** Markdown image whose src is a workspace file — loads through a signed asset URL. */function ThreadMarkdownImage(props: { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; readonly path: string; readonly alt: string | null; readonly onPressImage: (uri: string) => void;}) { const assetUrl = useAssetUrlState(props.environmentId, { _tag: "workspace-file", threadId: props.threadId, path: props.path, }); return ( <ThreadMarkdownImageView uri={assetUrl._tag === "Success" ? assetUrl.url : null} sourceKey={props.path} unavailable={assetUrl._tag === "Failure"} alt={props.alt} onPressImage={props.onPressImage} /> );} function ThreadMarkdownImageUnavailable(props: { readonly alt: string | null }) { return ( <ThreadMarkdownImageView uri={null} sourceKey="unavailable" unavailable alt={props.alt} onPressImage={() => undefined} /> );} 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 tryOpenExternalUrl(props.href, "markdown-link"); }} 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" > <NativeTextLines 1–500 of 2249. Load more
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 { classifyMarkdownImageSource } from "@t3tools/client-runtime/markdown-images"; 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, Platform, type LayoutChangeEvent, type NativeScrollEvent, type NativeSyntheticEvent, Pressable, ScrollView, StyleSheet, Text as NativeText, type ColorValue, useWindowDimensions, View, type ViewStyle, } 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, type SharedValue } from "react-native-reanimated"; import { useThemeColor } from "../../lib/useThemeColor"; import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics"; import { useFontFamily } from "../../lib/useFontFamily"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; import { hasWideMarkdownBlock } from "../../lib/wideMarkdownBlocks"; import { hasNativeSelectableMarkdownText, SelectableMarkdownText, type MarkdownImageRenderer, 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, deriveThreadFeedInitialContentInset, type LayoutVariant, } from "../../lib/layout"; import { resolveMarkdownFontSizes, resolveNativeMarkdownTypography, scaledTypographyLineHeight, } from "../../lib/appearancePreferences"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; 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 { resolveThreadFeedLiveFollow, type ThreadFeedLiveFollowEvent, } from "./thread-feed-live-follow"; import { collapsedWorkLogHeight, ThreadWorkGroupToggle, ThreadWorkLog, WORK_GROUP_TOGGLE_HEIGHT, } from "./thread-work-log"; import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState"; import { useAssetUrl, useAssetUrlState } from "../../state/assets"; import { resolveWorkspaceRelativeFilePath } from "../files/filePath"; import { MARKDOWN_IMAGE_MAX_WIDTH, resolveMarkdownImageDisplaySize } from "./markdownImageSize"; const WIDE_MARKDOWN_BLOCK_OPTIONS = { includeOrderedLists: Platform.OS === "android", } as const; 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); } // Pre-measurement heights for getFixedItemSize, mirroring renderFeedEntry's // classNames. The fold row's min-h-11 (44px) stays taller than its single // text-sm line at every supported base font size (26px at the 22pt maximum), // so its height is a constant; a drifted value costs one correction on // measure, not a persistent offset. const TURN_FOLD_HEIGHT = 56; // min-h-11 (44) + mb-3 (12) // The working row has no min-height clamp — its height follows the scaled // text-xs line height (see workingRowHeight in ThreadFeed). const WORKING_ROW_VERTICAL_EXTRAS = 24; // py-1 (8) + mb-4 (16) // 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 submittedMessageId: 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 onEndFollowEnabledChange?: (enabled: boolean) => void; readonly skills?: ReadonlyArray<SelectableMarkdownSkill>; /** Non-null when older turns exist beyond the loaded window. */ readonly loadEarlier?: { readonly loading: boolean; readonly onLoadEarlier: () => void; } | null; } 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> ); } function ThreadMarkdownImageView(props: { readonly uri: string | null; readonly sourceKey: string; readonly unavailable: boolean; readonly alt: string | null; readonly onPressImage: (uri: string) => void; }) { const codeBackground = useThemeColor("--color-md-code-bg"); const [availableWidth, setAvailableWidth] = useState(0); const [sourceSize, setSourceSize] = useState<{ width: number; height: number } | null>(null); const [failedUri, setFailedUri] = useState<string | null>(null); useEffect(() => { setSourceSize(null); }, [props.sourceKey]); useEffect(() => { setFailedUri(null); }, [props.uri]); const displaySize = sourceSize === null ? null : resolveMarkdownImageDisplaySize({ sourceWidth: sourceSize.width, sourceHeight: sourceSize.height, availableWidth, }); const failed = props.unavailable || (props.uri !== null && failedUri === props.uri); const placeholderWidth: ViewStyle["width"] = availableWidth > 0 ? Math.min(availableWidth, MARKDOWN_IMAGE_MAX_WIDTH) : "100%"; const frameStyle: ViewStyle = displaySize ?? { width: placeholderWidth, aspectRatio: 16 / 9 }; return ( <View onLayout={(event) => setAvailableWidth(event.nativeEvent.layout.width)} style={{ alignSelf: "stretch", gap: 6 }} > {props.uri === null || failed ? ( <View style={{ ...frameStyle, borderRadius: 10, backgroundColor: codeBackground, alignItems: "center", justifyContent: "center", }} > {failed ? ( <Text className="text-xs text-foreground-muted">Image unavailable</Text> ) : ( <ActivityIndicator /> )} </View> ) : ( <TouchableOpacity accessibilityRole="imagebutton" accessibilityLabel={props.alt ?? "Markdown image"} activeOpacity={0.7} onPress={() => props.onPressImage(props.uri!)} style={{ alignSelf: "flex-start" }} > <View style={{ ...frameStyle, borderRadius: 10, backgroundColor: codeBackground, alignItems: "center", justifyContent: "center", overflow: "hidden", }} > <ThreadMarkdownImageRequest key={props.uri} uri={props.uri} onLoad={setSourceSize} onError={() => setFailedUri(props.uri)} /> </View> </TouchableOpacity> )} {props.alt ? ( <Text selectable className="text-xs text-foreground-muted"> {props.alt} </Text> ) : null} </View> ); } function ThreadMarkdownImageRequest(props: { readonly uri: string; readonly onLoad: (sourceSize: { width: number; height: number }) => void; readonly onError: () => void; }) { const [loaded, setLoaded] = useState(false); return ( <> <Image source={{ uri: props.uri }} resizeMode="contain" accessible={false} onLoad={(event) => { setLoaded(true); props.onLoad(event.nativeEvent.source); }} onError={props.onError} style={{ width: "100%", height: "100%", opacity: loaded ? 1 : 0 }} /> {loaded ? null : ( <View pointerEvents="none" style={[StyleSheet.absoluteFill, { alignItems: "center", justifyContent: "center" }]} > <Text className="text-xs text-foreground-muted">Loading image…</Text> </View> )} </> ); } /** Markdown image whose src is a workspace file — loads through a signed asset URL. */ function ThreadMarkdownImage(props: { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; readonly path: string; readonly alt: string | null; readonly onPressImage: (uri: string) => void; }) { const assetUrl = useAssetUrlState(props.environmentId, { _tag: "workspace-file", threadId: props.threadId, path: props.path, }); return ( <ThreadMarkdownImageView uri={assetUrl._tag === "Success" ? assetUrl.url : null} sourceKey={props.path} unavailable={assetUrl._tag === "Failure"} alt={props.alt} onPressImage={props.onPressImage} /> ); } function ThreadMarkdownImageUnavailable(props: { readonly alt: string | null }) { return ( <ThreadMarkdownImageView uri={null} sourceKey="unavailable" unavailable alt={props.alt} onPressImage={() => undefined} /> ); } 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 tryOpenExternalUrl(props.href, "markdown-link"); }} 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