From 415e69f834ca8869a2872b8d901e9161e3763d1a Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:26:53 -0700 Subject: [PATCH] perf(web): snapshot coordinator keeps the last route target in state Co-Authored-By: Claude Opus 5.5 --- apps/web/reactCompiledFiles.ts | 1 + .../desktop/SnapShotCoordinator.tsx | 103 +++++++++++------- 2 files changed, 67 insertions(+), 37 deletions(-) diff --git a/apps/web/reactCompiledFiles.ts b/apps/web/reactCompiledFiles.ts index f41f4227d6e89..aa87a56659e09 100644 --- a/apps/web/reactCompiledFiles.ts +++ b/apps/web/reactCompiledFiles.ts @@ -41,4 +41,5 @@ export const REACT_COMPILED_FILES = [ "apps/web/src/components/pullRequest/usePullRequestFilesViewed.ts", "apps/web/src/hooks/useLiveRefresh.ts", "apps/web/src/browser/useBrowserViewportResize.ts", + "apps/web/src/components/desktop/SnapShotCoordinator.tsx", ] as const; diff --git a/apps/web/src/components/desktop/SnapShotCoordinator.tsx b/apps/web/src/components/desktop/SnapShotCoordinator.tsx index 19afa2323fc09..429667b09e841 100644 --- a/apps/web/src/components/desktop/SnapShotCoordinator.tsx +++ b/apps/web/src/components/desktop/SnapShotCoordinator.tsx @@ -1,9 +1,10 @@ import { type DesktopPendingSnapShot, + type DesktopSnapShotEvent, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, type ScopedThreadRef, } from "@t3tools/contracts"; -import { useCallback, useEffect, useRef } from "react"; +import { useEffect, useEffectEvent, useRef, useState } from "react"; import { type DraftId, @@ -11,6 +12,7 @@ import { useComposerDraftStore, } from "../../composerDraftStore"; import { useHandleNewThread } from "../../hooks/useHandleNewThread"; +import { useStableCallback } from "../../hooks/useStableCallback"; import { useClientSettings } from "../../hooks/useSettings"; import { readThreadShell } from "../../state/entities"; import { compressImageToByteLimit, dataUrlToFile } from "../../lib/imageCompression"; @@ -36,6 +38,11 @@ import { stackedThreadToast, toastManager } from "../ui/toast"; type CaptureTarget = DraftId | ScopedThreadRef; +function isSameCaptureTarget(a: CaptureTarget, b: CaptureTarget): boolean { + if (typeof a === "string" || typeof b === "string") return a === b; + return a.environmentId === b.environmentId && a.threadId === b.threadId; +} + export function resolveExistingSnapShotTarget( target: CaptureTarget, routeThreadRef: ScopedThreadRef | null, @@ -207,25 +214,48 @@ export function SnapShotCoordinator() { ); const animateCaptures = useClientSettings((settings) => settings.snapShotAnimations); const captureTargetsRef = useRef(new Map>()); - const lastTargetRef = useRef(null); const targetResolutionRef = useRef | null>(null); const drainingRef = useRef | null>(null); const rerunRequestedRef = useRef(false); const soundedCaptureIdsRef = useRef(new Set()); const pendingAnimationStartsRef = useRef(new Set()); + // Captures go to the last thread or draft on screen, even from routes that show neither. const currentTarget = routeThreadRef ?? routeDraftId; - if (currentTarget) lastTargetRef.current = currentTarget; + const [lastRouteTarget, setLastRouteTarget] = useState(currentTarget); + if ( + currentTarget && + (lastRouteTarget === null || !isSameCaptureTarget(currentTarget, lastRouteTarget)) + ) { + setLastRouteTarget(currentTarget); + } + // What a resolution settled on (a promoted draft, a new draft, or nothing) stands in for the + // route's target until the route moves on, so captures in one drain share one new draft. + const settledTargetRef = useRef<{ + readonly routeTarget: CaptureTarget | null; + readonly target: CaptureTarget | null; + } | null>(null); + const readLastRouteTarget = useStableCallback(() => lastRouteTarget); + // A resolution records what it settled on against the route it started from, so a + // route change while it is pending cannot hand that draft to the new route. + const readSettledTarget = (routeTarget: CaptureTarget | null) => { + const settled = settledTargetRef.current; + return settled && settled.routeTarget === routeTarget ? settled.target : routeTarget; + }; + const settleTarget = (routeTarget: CaptureTarget | null, target: CaptureTarget | null) => { + settledTargetRef.current = { routeTarget, target }; + }; - const resolveTarget = useCallback(async (): Promise => { - const lastTarget = lastTargetRef.current; + const resolveTarget = async (): Promise => { + const routeTarget = readLastRouteTarget(); + const lastTarget = readSettledTarget(routeTarget); if (lastTarget) { const existingTarget = resolveExistingSnapShotTarget(lastTarget, routeThreadRef); if (existingTarget) { - lastTargetRef.current = existingTarget; + settleTarget(routeTarget, existingTarget); return existingTarget; } - lastTargetRef.current = null; + settleTarget(routeTarget, null); } const projectRef = resolveThreadActionProjectRef({ activeDraftThread, @@ -236,27 +266,21 @@ export function SnapShotCoordinator() { if (!projectRef) return null; const created = await handleNewThread(projectRef); if (!created) return null; - lastTargetRef.current = created.draftId; + settleTarget(routeTarget, created.draftId); return created.draftId; - }, [activeDraftThread, activeThread, defaultProjectRef, handleNewThread, routeThreadRef]); + }; - const resolveCaptureTarget = useCallback( - () => resolveSnapShotTargetOnce(targetResolutionRef, resolveTarget), - [resolveTarget], - ); + const resolveCaptureTarget = () => resolveSnapShotTargetOnce(targetResolutionRef, resolveTarget); - const playCaptureSound = useCallback( - (id: string) => { - if (!captureSound || soundedCaptureIdsRef.current.has(id)) return; - soundedCaptureIdsRef.current.add(id); - try { - playSnapShotSound(captureSound); - } catch {} - }, - [captureSound], - ); + const playCaptureSound = (id: string) => { + if (!captureSound || soundedCaptureIdsRef.current.has(id)) return; + soundedCaptureIdsRef.current.add(id); + try { + playSnapShotSound(captureSound); + } catch {} + }; - const drain = useCallback(async () => { + const drain = async () => { const bridge = getDesktopSnapShotBridge(); if (!bridge) return; if (drainingRef.current) { @@ -326,16 +350,16 @@ export function SnapShotCoordinator() { }); drainingRef.current = operation; return operation; - }, [playCaptureSound, resolveCaptureTarget, routeThreadRef]); + }; - useEffect(() => { - const bridge = getDesktopSnapShotBridge(); - if (!bridge) return; - void drain(); - const unsubscribe = bridge.onSnapShotEvent((event) => { + // Subscriptions read the latest handlers through effect events, so they attach once + // instead of restarting (and re-draining) whenever a handler's inputs change. + const drainPending = useEffectEvent(() => drain()); + const handleSnapShotEvent = useEffectEvent( + (bridge: DesktopSnapShotBridge, event: DesktopSnapShotEvent) => { switch (event.type) { case "requested": { - const current = lastTargetRef.current; + const current = readSettledTarget(readLastRouteTarget()); const target = current ? resolveExistingSnapShotTarget(current, routeThreadRef) : null; // Creating a new draft would navigate the renderer before a self-capture finishes. if (target) { @@ -384,19 +408,24 @@ export function SnapShotCoordinator() { case "shortcut-changed": return; } - }); - return unsubscribe; - }, [animateCaptures, drain, playCaptureSound, resolveCaptureTarget, routeThreadRef]); + }, + ); + useEffect(() => { + const bridge = getDesktopSnapShotBridge(); + if (!bridge) return; + void drainPending(); + return bridge.onSnapShotEvent((event) => handleSnapShotEvent(bridge, event)); + }, []); useEffect(() => { const dismissOnBlur = () => { pendingAnimationStartsRef.current.clear(); dismissAllSnapShotAnimations(); }; - const drainOnFocus = () => void drain(); + const drainOnFocus = () => void drainPending(); const handleVisibilityChange = () => { if (document.visibilityState === "hidden") dismissOnBlur(); - else void drain(); + else void drainPending(); }; window.addEventListener("blur", dismissOnBlur); window.addEventListener("focus", drainOnFocus); @@ -406,7 +435,7 @@ export function SnapShotCoordinator() { window.removeEventListener("focus", drainOnFocus); document.removeEventListener("visibilitychange", handleVisibilityChange); }; - }, [drain]); + }, []); return null; }