| @@ -1,6 +1,6 @@ |
| 1 | 1 | import * as Haptics from "expo-haptics"; |
| 2 | 2 | import { KeyboardAwareLegendList } from "@legendapp/list/keyboard"; |
| 3 | | −import { type LegendListRef } from "@legendapp/list/react-native"; |
| 3 | +import { useViewabilityAmount, type LegendListRef } from "@legendapp/list/react-native"; |
| 4 | 4 | import { scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| 5 | 5 | import { canForkProjectedAssistantItem } from "@t3tools/client-runtime/state/thread-workflows"; |
| 6 | 6 | import { |
| @@ -79,7 +79,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; · 72 unmodified lines |
| 79 | 79 | import Animated, { |
| 80 | 80 | FadeIn, |
| 81 | 81 | FadeInUp, |
| 82 | | − FadeOut, |
| 83 | 82 | LinearTransition, |
| 84 | 83 | type SharedValue, |
| 85 | 84 | } from "react-native-reanimated"; |
| @@ -150,13 +149,19 @@ import { · 64 unmodified lines |
| 150 | 149 | type ThreadFeedLiveFollowEvent, |
| 151 | 150 | } from "./thread-feed-live-follow"; |
| 152 | 151 | import { |
| 152 | + ThreadDisclosureChevron, |
| 153 | 153 | THREAD_DISCLOSURE_TRANSITION_MS, |
| 154 | 154 | ThreadWorkGroupToggle, |
| 155 | 155 | ThreadWorkLog, |
| 156 | 156 | } from "./thread-work-log"; |
| 157 | 157 | import { resolveThreadFeedFixedItemSize } from "./thread-feed-item-size"; |
| 158 | 158 | import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState"; |
| 159 | | −import { assetEnvironment, useAssetUrl, useAssetUrlState } from "../../state/assets"; |
| 159 | +import { |
| 160 | + assetEnvironment, |
| 161 | + useAssetUrl, |
| 162 | + useAssetUrlState, |
| 163 | + useRefreshAssetUrl, |
| 164 | +} from "../../state/assets"; |
| 160 | 165 | import { useAtomQueryRunner } from "../../state/use-atom-query-runner"; |
| 161 | 166 | import { usePreparedConnection } from "../../state/session"; |
| 162 | 167 | import * as Option from "effect/Option"; |
| @@ -191,9 +196,10 @@ function formatMessageTime(input: string): string { · 28 unmodified lines |
| 191 | 196 | } |
| 192 | 197 | |
| 193 | 198 | const THREAD_FEED_LAYOUT_TRANSITION = LinearTransition.duration(THREAD_DISCLOSURE_TRANSITION_MS); |
| 194 | | −const THREAD_FEED_DISCLOSURE_ENTER_TRANSITION = FadeIn.duration(140); |
| 195 | | −const THREAD_FEED_DISCLOSURE_EXIT_TRANSITION = FadeOut.duration(120); |
| 196 | | −const EMPTY_DISCLOSURE_ENTRY_IDS: ReadonlySet<string> = new Set(); |
| 199 | +// Let neighboring rows move out of the new rows' space before showing their text. |
| 200 | +const THREAD_FEED_DISCLOSURE_ENTER_TRANSITION = FadeIn.delay( |
| 201 | + THREAD_DISCLOSURE_TRANSITION_MS, |
| 202 | +).duration(140); |
| 197 | 203 | |
| 198 | 204 | // Entering animations must only play for rows born just now — LegendList |
| 199 | 205 | // remounts rows when they scroll back into view, and replaying an entrance for |
| @@ -340,6 +346,7 @@ function MessageAttachmentFile(props: { · 140 unmodified lines |
| 340 | 346 | }) { |
| 341 | 347 | const sourceIdentifier = useId(); |
| 342 | 348 | const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, { |
| 349 | + refresh: true, |
| 343 | 350 | reportFailure: false, |
| 344 | 351 | }); |
| 345 | 352 | const preparedConnection = usePreparedConnection(props.environmentId); |
| @@ -557,9 +564,11 @@ function ThreadMarkdownImageView(props: { · 211 unmodified lines |
| 557 | 564 | readonly sourceKey: string; |
| 558 | 565 | readonly unavailable: boolean; |
| 559 | 566 | readonly alt: string | null; |
| 567 | + readonly actionsSource?: MediaActionsSource; |
| 560 | 568 | readonly onPressPreview: (source: FilePreviewSource) => void; |
| 561 | 569 | }) { |
| 562 | 570 | const sourceIdentifier = useId(); |
| 571 | + const mediaActions = useMediaActions(props.actionsSource); |
| 563 | 572 | const [availableWidth, setAvailableWidth] = useState(0); |
| 564 | 573 | const [sourceSize, setSourceSize] = useState<{ width: number; height: number } | null>(null); |
| 565 | 574 | const [failedUri, setFailedUri] = useState<string | null>(null); |
| @@ -602,36 +611,51 @@ function ThreadMarkdownImageView(props: { · 36 unmodified lines |
| 602 | 611 | ) : ( |
| 603 | 612 | <ActivityIndicator /> |
| 604 | 613 | )} |
| 614 | + {props.actionsSource ? ( |
| 615 | + <View className="absolute right-1 top-1"> |
| 616 | + <MediaActionsMenu media={mediaActions} /> |
| 617 | + </View> |
| 618 | + ) : null} |
| 605 | 619 | </View> |
| 606 | 620 | ) : ( |
| 607 | 621 | <PresentationSource identifier={sourceIdentifier} style={{ alignSelf: "flex-start" }}> |
| 608 | | − <Pressable |
| 609 | | − accessibilityRole="imagebutton" |
| 610 | | − accessibilityLabel={props.alt ?? "Markdown image"} |
| 611 | | − onPress={() => |
| 612 | | − props.onPressPreview({ |
| 613 | | − kind: "image", |
| 614 | | − uri: props.uri!, |
| 615 | | − name: props.alt ?? "Image", |
| 616 | | − sourceIdentifier, |
| 617 | | − }) |
| 618 | | − } |
| 619 | | − style={{ alignSelf: "flex-start" }} |
| 620 | | − > |
| 621 | | − <View |
| 622 | | − className="items-center justify-center overflow-hidden rounded-[10px] bg-md-code-bg" |
| 623 | | − style={{ |
| 624 | | − ...frameStyle, |
| 625 | | − }} |
| 626 | | − > |
| 627 | | − <ThreadMarkdownImageRequest |
| 628 | | − key={props.uri} |
| 629 | | − uri={props.uri} |
| 630 | | − onLoad={setSourceSize} |
| 631 | | − onError={() => setFailedUri(props.uri)} |
| 632 | | − /> |
| 633 | | − </View> |
| 634 | | − </Pressable> |
| 622 | + <View> |
| 623 | + <MediaActionsMenu media={mediaActions}> |
| 624 | + <Pressable |
| 625 | + accessibilityRole="imagebutton" |
| 626 | + accessibilityLabel={props.alt ?? "Markdown image"} |
| 627 | + onPress={() => |
| 628 | + props.onPressPreview({ |
| 629 | + kind: "image", |
| 630 | + uri: props.uri!, |
| 631 | + name: props.alt ?? "Image", |
| 632 | + sourceIdentifier, |
| 633 | + actionsSource: props.actionsSource, |
| 634 | + }) |
| 635 | + } |
| 636 | + style={{ alignSelf: "flex-start" }} |
| 637 | + > |
| 638 | + <View |
| 639 | + className="items-center justify-center overflow-hidden rounded-[10px] bg-md-code-bg" |
| 640 | + style={{ |
| 641 | + ...frameStyle, |
| 642 | + }} |
| 643 | + > |
| 644 | + <ThreadMarkdownImageRequest |
| 645 | + key={props.uri} |
| 646 | + uri={props.uri} |
| 647 | + onLoad={setSourceSize} |
| 648 | + onError={() => setFailedUri(props.uri)} |
| 649 | + /> |
| 650 | + </View> |
| 651 | + </Pressable> |
| 652 | + </MediaActionsMenu> |
| 653 | + {props.actionsSource ? ( |
| 654 | + <View className="absolute right-1 top-1"> |
| 655 | + <MediaActionsMenu media={mediaActions} /> |
| 656 | + </View> |
| 657 | + ) : null} |
| 658 | + </View> |
| 635 | 659 | </PresentationSource> |
| 636 | 660 | )} |
| 637 | 661 | {props.alt ? ( |
| @@ -678,9 +702,10 @@ function ThreadMarkdownImageRequest(props: { · 40 unmodified lines |
| 678 | 702 | /** Environment-hosted image that loads through a signed asset URL. */ |
| 679 | 703 | function ThreadMarkdownImage(props: { |
| 680 | 704 | readonly environmentId: EnvironmentId; |
| 681 | | − readonly resource: Extract<AssetResource, { readonly _tag: "attachment" | "workspace-file" }>; |
| 705 | + readonly resource: Extract<AssetResource, { readonly _tag: "attachment" | "media-file" }>; |
| 682 | 706 | readonly alt: string | null; |
| 683 | 707 | readonly srcFragment?: string; |
| 708 | + readonly actionsSource?: MediaActionsSource; |
| 684 | 709 | readonly onPressPreview: (source: FilePreviewSource) => void; |
| 685 | 710 | }) { |
| 686 | 711 | const assetUrl = useAssetUrlState(props.environmentId, props.resource); |
| @@ -695,11 +720,59 @@ function ThreadMarkdownImage(props: { · 8 unmodified lines |
| 695 | 720 | } |
| 696 | 721 | unavailable={assetUrl._tag === "Failure"} |
| 697 | 722 | alt={props.alt} |
| 723 | + actionsSource={props.actionsSource} |
| 698 | 724 | onPressPreview={props.onPressPreview} |
| 699 | 725 | /> |
| 700 | 726 | ); |
| 701 | 727 | } |
| 702 | 728 | |
| 729 | +const ThreadMediaVisibleContext = createContext(false); |
| 730 | +// LegendList only computes hook visibility when the list has a viewability config. |
| 731 | +const THREAD_MEDIA_VIEWABILITY_CONFIG = { itemVisiblePercentThreshold: 0 }; |
| 732 | + |
| 733 | +function ThreadMediaVisibility(props: { readonly children: ReactNode }) { |
| 734 | + const [visible, setVisible] = useState(false); |
| 735 | + useViewabilityAmount<ThreadFeedEntry>( |
| 736 | + useCallback((token) => setVisible(token.sizeVisible > 0), []), |
| 737 | + ); |
| 738 | + return <ThreadMediaVisibleContext value={visible}>{props.children}</ThreadMediaVisibleContext>; |
| 739 | +} |
| 740 | + |
| 741 | +function ThreadMarkdownVideo(props: { |
| 742 | + readonly source: MediaVideoPreviewSource; |
| 743 | + readonly onExpand: (source: MediaVideoPreviewSource) => void; |
| 744 | +}) { |
| 745 | + const { source } = props; |
| 746 | + const visible = useContext(ThreadMediaVisibleContext); |
| 747 | + const thumbnailKey = mediaVideoThumbnailKey(source); |
| 748 | + const asset = useAssetUrlState( |
| 749 | + "environmentId" in source ? source.environmentId : null, |
| 750 | + "resource" in source ? source.resource : null, |
| 751 | + ); |
| 752 | + const refreshAssetUrl = useRefreshAssetUrl( |
| 753 | + "environmentId" in source ? source.environmentId : null, |
| 754 | + "resource" in source ? source.resource : null, |
| 755 | + ); |
| 756 | + const uri = mediaVideoPreviewUri(source, asset._tag === "Success" ? asset.url : null); |
| 757 | + return ( |
| 758 | + <MediaVideoPlayer |
| 759 | + key={thumbnailKey} |
| 760 | + uri={uri} |
| 761 | + resolvePlaybackUri={ |
| 762 | + "resource" in source |
| 763 | + ? async () => mediaVideoPreviewUri(source, await refreshAssetUrl()) |
| 764 | + : undefined |
| 765 | + } |
| 766 | + name={source.name} |
| 767 | + thumbnailKey={thumbnailKey} |
| 768 | + thumbnailVisible={visible} |
| 769 | + unavailable={"resource" in source && asset._tag === "Failure"} |
| 770 | + actionsSource={source.actionsSource} |
| 771 | + onExpand={() => props.onExpand(source)} |
| 772 | + /> |
| 773 | + ); |
| 774 | +} |
| 775 | + |
| 703 | 776 | function ThreadMarkdownImageUnavailable(props: { readonly alt: string | null }) { |
| 704 | 777 | return ( |
| 705 | 778 | <ThreadMarkdownImageView |
| @@ -1287,21 +1360,16 @@ function useMarkdownStyles( · 581 unmodified lines |
| 1287 | 1360 | title: node.title ?? null, |
| 1288 | 1361 | }) ?? undefined) |
| 1289 | 1362 | : undefined, |
| 1290 | | − code_inline: ({ content }) => { |
| 1291 | | − const value = content ?? ""; |
| 1292 | | − return ( |
| 1293 | | − <NativeText |
| 1294 | | − className="font-mono" |
| 1295 | | − style={{ |
| 1296 | | − color: inlineCodeTextColor, |
| 1297 | | − fontSize: markdownFontSizes.codeBlockFontSize, |
| 1298 | | − lineHeight: markdownFontSizes.bodyLineHeight, |
| 1299 | | − }} |
| 1300 | | − > |
| 1301 | | − {value} |
| 1302 | | − </NativeText> |
| 1303 | | − ); |
| 1304 | | − }, |
| 1363 | + code_inline: ({ content }) => ( |
| 1364 | + <MarkdownInlineCode |
| 1365 | + content={content ?? ""} |
| 1366 | + textColor={inlineTextColor} |
| 1367 | + codeColor={inlineCodeTextColor} |
| 1368 | + fontSize={markdownFontSizes.codeBlockFontSize} |
| 1369 | + lineHeight={markdownFontSizes.bodyLineHeight} |
| 1370 | + onLinkPress={onLinkPress} |
| 1371 | + /> |
| 1372 | + ), |
| 1305 | 1373 | ...(preserveSoftBreaks |
| 1306 | 1374 | ? { |
| 1307 | 1375 | soft_break: () => <NativeText>{"\n"}</NativeText>, |
| @@ -1504,7 +1572,7 @@ function renderFeedEntry( · 196 unmodified lines |
| 1504 | 1572 | accessibilityState={{ expanded: entry.expanded }} |
| 1505 | 1573 | onPress={() => props.onToggleTurnFold(entry.runId)} |
| 1506 | 1574 | hitSlop={4} |
| 1507 | | − className="mb-3 min-h-11 flex-row items-center gap-2 border-b border-adaptive-neutral-200-a80-white-a8 px-2" |
| 1575 | + className="mb-1 min-h-11 flex-row items-center gap-2 border-b border-adaptive-neutral-200-a80-white-a8 px-2" |
| 1508 | 1576 | > |
| 1509 | 1577 | <Text |
| 1510 | 1578 | key={props.workRowSizing.textSizeKey} |
| @@ -1516,8 +1584,7 @@ function renderFeedEntry( · 5 unmodified lines |
| 1516 | 1584 | expanded={entry.expanded} |
| 1517 | 1585 | collapsedDirection="right" |
| 1518 | 1586 | size={15} |
| 1519 | | − tintColorClassName={"accent-icon-subtle"} |
| 1520 | | − type="monochrome" |
| 1587 | + tintColor={iconSubtleColor} |
| 1521 | 1588 | /> |
| 1522 | 1589 | </Pressable> |
| 1523 | 1590 | ); |
| @@ -2092,7 +2159,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 568 unmodified lines |
| 2092 | 2159 | const [endFollowEnabled, setEndFollowEnabled] = useState(true); |
| 2093 | 2160 | const endFollowEnabledRef = useRef(true); |
| 2094 | 2161 | // A "user scroll session" spans from drag start through the end of its |
| 2095 | | − // momentum; only motion inside a session can break follow, so MVCP |
| 2162 | + // momentum; scroll events only break follow inside that session, so MVCP |
| 2096 | 2163 | // compensations and programmatic scrolls never strand a follower. |
| 2097 | 2164 | const userScrollSessionRef = useRef(false); |
| 2098 | 2165 | const setEndFollow = useCallback( |
| @@ -2253,14 +2320,30 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 154 unmodified lines |
| 2253 | 2320 | ); |
| 2254 | 2321 | const renderMarkdownImage = useCallback<MarkdownImageRenderer>( |
| 2255 | 2322 | (image) => { |
| 2323 | + const media = resolveMarkdownMediaPreview(image.href, { |
| 2324 | + environmentId: props.environmentId, |
| 2325 | + threadId: props.threadId, |
| 2326 | + workspaceRoot: props.workspaceRoot, |
| 2327 | + imageEmbed: true, |
| 2328 | + }); |
| 2329 | + if (media?.kind === "video") { |
| 2330 | + return ( |
| 2331 | + <ThreadMarkdownVideo |
| 2332 | + key={image.href} |
| 2333 | + source={{ ...media.source, name: image.alt ?? media.source.name }} |
| 2334 | + onExpand={(source) => setExpandedVideo((current) => current ?? source)} |
| 2335 | + /> |
| 2336 | + ); |
| 2337 | + } |
| 2256 | 2338 | const imageSource = classifyMarkdownImageSource(image.href, props.workspaceRoot ?? null); |
| 2257 | 2339 | if (imageSource._tag === "Direct") { |
| 2258 | 2340 | return ( |
| 2259 | 2341 | <ThreadMarkdownImageView |
| 2260 | | − uri={imageSource.uri} |
| 2342 | + uri={normalizeNativeMarkdownUrl(imageSource.uri)} |
| 2261 | 2343 | sourceKey={imageSource.uri} |
| 2262 | 2344 | unavailable={false} |
| 2263 | 2345 | alt={image.alt} |
| 2346 | + actionsSource={media?.source.actionsSource} |
| 2264 | 2347 | onPressPreview={(source) => setExpandedFile((current) => current ?? source)} |
| 2265 | 2348 | /> |
| 2266 | 2349 | ); |
| @@ -2272,12 +2355,13 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 5 unmodified lines |
| 2272 | 2355 | <ThreadMarkdownImage |
| 2273 | 2356 | environmentId={props.environmentId} |
| 2274 | 2357 | resource={{ |
| 2275 | | − _tag: "workspace-file", |
| 2358 | + _tag: "media-file", |
| 2276 | 2359 | threadId: props.threadId, |
| 2277 | 2360 | path: imageSource.path, |
| 2278 | 2361 | }} |
| 2279 | 2362 | alt={image.alt} |
| 2280 | 2363 | srcFragment={markdownImageSourceFragment(image.href)} |
| 2364 | + actionsSource={media?.source.actionsSource} |
| 2281 | 2365 | onPressPreview={(source) => setExpandedFile((current) => current ?? source)} |
| 2282 | 2366 | /> |
| 2283 | 2367 | ); |
| @@ -2290,12 +2374,26 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 6 unmodified lines |
| 2290 | 2374 | threadId: props.threadId, |
| 2291 | 2375 | workspaceRoot: props.workspaceRoot, |
| 2292 | 2376 | }); |
| 2377 | + const media = viewedImage |
| 2378 | + ? resolveMarkdownMediaPreview(image.href, { |
| 2379 | + environmentId: props.environmentId, |
| 2380 | + threadId: props.threadId, |
| 2381 | + workspaceRoot: props.workspaceRoot, |
| 2382 | + imageEmbed: true, |
| 2383 | + }) |
| 2384 | + : null; |
| 2385 | + const actionsSource = media?.source.actionsSource; |
| 2293 | 2386 | return viewedImage ? ( |
| 2294 | 2387 | <ThreadMarkdownImage |
| 2295 | 2388 | environmentId={props.environmentId} |
| 2296 | 2389 | resource={viewedImage.resource} |
| 2297 | 2390 | alt={viewedImage.alt} |
| 2298 | 2391 | srcFragment={viewedImage.srcFragment} |
| 2392 | + actionsSource={ |
| 2393 | + actionsSource && "resource" in actionsSource |
| 2394 | + ? { ...actionsSource, resource: viewedImage.resource } |
| 2395 | + : undefined |
| 2396 | + } |
| 2299 | 2397 | onPressPreview={(source) => setExpandedFile((current) => current ?? source)} |
| 2300 | 2398 | /> |
| 2301 | 2399 | ) : null; |
| @@ -2572,13 +2670,23 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 270 unmodified lines |
| 2572 | 2670 | } |
| 2573 | 2671 | disclosureSettleFrameRef.current = requestAnimationFrame(() => { |
| 2574 | 2672 | disclosureSettleSecondFrameRef.current = requestAnimationFrame(() => { |
| 2673 | + // A disclosure can leave the reader above the end without a drag. |
| 2674 | + // Reconcile follow before a later layout or resume can re-pin it. |
| 2675 | + const listState = props.listRef.current?.getState(); |
| 2676 | + if (listState) { |
| 2677 | + transitionEndFollow({ |
| 2678 | + type: "disclosure-settled", |
| 2679 | + isAtEnd: listState.isAtEnd, |
| 2680 | + userScrollSessionActive: userScrollSessionRef.current, |
| 2681 | + }); |
| 2682 | + } |
| 2575 | 2683 | disclosureAnchorKeyRef.current = null; |
| 2576 | 2684 | setDisclosureToggleSettling(false); |
| 2577 | 2685 | disclosureSettleFrameRef.current = null; |
| 2578 | 2686 | disclosureSettleSecondFrameRef.current = null; |
| 2579 | 2687 | }); |
| 2580 | 2688 | }); |
| 2581 | | − }, []); |
| 2689 | + }, [props.listRef, transitionEndFollow]); |
| 2582 | 2690 | |
| 2583 | 2691 | const suspendEndScrollMaintenanceForDisclosure = useCallback((anchorKey: string | null) => { |
| 2584 | 2692 | disclosureAnchorKeyRef.current = anchorKey; |
| @@ -2713,44 +2821,41 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 128 unmodified lines |
| 2713 | 2821 | (info: { item: ThreadFeedEntry; index: number }) => ( |
| 2714 | 2822 | <Animated.View |
| 2715 | 2823 | key={info.item.id} |
| 2716 | | − entering={ |
| 2717 | | − disclosureEnteringEntryIds.has(info.item.id) |
| 2718 | | − ? THREAD_FEED_DISCLOSURE_ENTER_TRANSITION |
| 2719 | | − : undefined |
| 2720 | | − } |
| 2721 | | − exiting={THREAD_FEED_DISCLOSURE_EXIT_TRANSITION} |
| 2824 | + entering={disclosureToggleSettling ? THREAD_FEED_DISCLOSURE_ENTER_TRANSITION : undefined} |
| 2722 | 2825 | > |
| 2723 | | − {renderFeedEntry(info, { |
| 2724 | | − environmentId: props.environmentId, |
| 2725 | | − threadId: props.threadId, |
| 2726 | | − copiedRowId, |
| 2727 | | − expandedWorkRows, |
| 2728 | | − terminalAssistantMessageIds, |
| 2729 | | − unsettledTurnId, |
| 2730 | | − onCopyWorkRow, |
| 2731 | | − onToggleWorkGroup, |
| 2732 | | − onToggleWorkRow, |
| 2733 | | − onToggleTurnFold, |
| 2734 | | − onPressPreview, |
| 2735 | | − onPressVideo, |
| 2736 | | − onMarkdownLinkPress, |
| 2737 | | − renderMarkdownImage, |
| 2738 | | − renderViewedImage, |
| 2739 | | − iconSubtleColor, |
| 2740 | | − userBubbleColor, |
| 2741 | | − markdownStyles, |
| 2742 | | − reviewCommentColors, |
| 2743 | | − reviewCommentBubbleWidth, |
| 2744 | | − userBubbleMaxWidth, |
| 2745 | | − threadTitle: props.threadTitle, |
| 2746 | | − skills: props.skills, |
| 2747 | | − workspaceRoot: props.workspaceRoot, |
| 2748 | | − })} |
| 2826 | + <ThreadMediaVisibility> |
| 2827 | + {renderFeedEntry(info, { |
| 2828 | + environmentId: props.environmentId, |
| 2829 | + threadId: props.threadId, |
| 2830 | + copiedRowId, |
| 2831 | + expandedWorkRows, |
| 2832 | + terminalAssistantMessageIds, |
| 2833 | + unsettledTurnId, |
| 2834 | + onCopyWorkRow, |
| 2835 | + onToggleWorkGroup, |
| 2836 | + onToggleWorkRow, |
| 2837 | + onToggleTurnFold, |
| 2838 | + onPressPreview, |
| 2839 | + onPressVideo, |
| 2840 | + onMarkdownLinkPress, |
| 2841 | + renderMarkdownImage, |
| 2842 | + renderViewedImage, |
| 2843 | + iconSubtleColor, |
| 2844 | + userBubbleColor, |
| 2845 | + markdownStyles, |
| 2846 | + reviewCommentColors, |
| 2847 | + reviewCommentBubbleWidth, |
| 2848 | + userBubbleMaxWidth, |
| 2849 | + threadTitle: props.threadTitle, |
| 2850 | + skills: props.skills, |
| 2851 | + workspaceRoot: props.workspaceRoot, |
| 2852 | + })} |
| 2853 | + </ThreadMediaVisibility> |
| 2749 | 2854 | </Animated.View> |
| 2750 | 2855 | ), |
| 2751 | 2856 | [ |
| 2752 | 2857 | copiedRowId, |
| 2753 | | − disclosureEnteringEntryIds, |
| 2858 | + disclosureToggleSettling, |
| 2754 | 2859 | expandedWorkRows, |
| 2755 | 2860 | workRowSizing, |
| 2756 | 2861 | workGroupScrollPositions, |
| @@ -2863,6 +2968,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 106 unmodified lines |
| 2863 | 2968 | data={presentedFeed} |
| 2864 | 2969 | extraData={listAppearanceData} |
| 2865 | 2970 | renderItem={renderItem} |
| 2971 | + viewabilityConfig={THREAD_MEDIA_VIEWABILITY_CONFIG} |
| 2866 | 2972 | keyExtractor={(entry) => entry.id} |
| 2867 | 2973 | getItemType={(entry) => |
| 2868 | 2974 | entry.type === "message" ? `message:${entry.message.role}` : entry.type |
| @@ -2938,4 +3044,5 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 69 unmodified lines |
| 2938 | 3044 | <FilePreviewModal source={expandedFile} onRequestClose={() => setExpandedFile(null)} /> |
| 2939 | 3045 | </> |
| 2940 | 3046 | ); |
| 3047 | + const EMPTY_DISCLOSURE_ENTRY_IDS: ReadonlySet<string> = new Set(); |
| 2941 | 3048 | }); |