Skip to content

Commit 2522ecf

Browse files
perf(web): snapshot coordinator keeps the last route target in state
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 3161332 commit 2522ecf

2 files changed

Lines changed: 54 additions & 27 deletions

File tree

‎apps/web/reactCompiledFiles.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ export const REACT_COMPILED_FILES = [
4141
"apps/web/src/components/pullRequest/usePullRequestFilesViewed.ts",
4242
"apps/web/src/hooks/useLiveRefresh.ts",
4343
"apps/web/src/browser/useBrowserViewportResize.ts",
44+
"apps/web/src/components/desktop/SnapShotCoordinator.tsx",
4445
] as const;

‎apps/web/src/components/desktop/SnapShotCoordinator.tsx‎

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import {
33
PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
44
type ScopedThreadRef,
55
} from "@t3tools/contracts";
6-
import { useCallback, useEffect, useRef } from "react";
6+
import { useEffect, useRef, useState } from "react";
77

88
import {
99
type DraftId,
1010
type PersistedComposerImageAttachment,
1111
useComposerDraftStore,
1212
} from "../../composerDraftStore";
1313
import { useHandleNewThread } from "../../hooks/useHandleNewThread";
14+
import { useStableCallback } from "../../hooks/useStableCallback";
1415
import { useClientSettings } from "../../hooks/useSettings";
1516
import { readThreadShell } from "../../state/entities";
1617
import { compressImageToByteLimit, dataUrlToFile } from "../../lib/imageCompression";
@@ -36,6 +37,11 @@ import { stackedThreadToast, toastManager } from "../ui/toast";
3637

3738
type CaptureTarget = DraftId | ScopedThreadRef;
3839

40+
function isSameCaptureTarget(a: CaptureTarget, b: CaptureTarget): boolean {
41+
if (typeof a === "string" || typeof b === "string") return a === b;
42+
return a.environmentId === b.environmentId && a.threadId === b.threadId;
43+
}
44+
3945
export function resolveExistingSnapShotTarget(
4046
target: CaptureTarget,
4147
routeThreadRef: ScopedThreadRef | null,
@@ -207,25 +213,44 @@ export function SnapShotCoordinator() {
207213
);
208214
const animateCaptures = useClientSettings((settings) => settings.snapShotAnimations);
209215
const captureTargetsRef = useRef(new Map<string, Promise<CaptureTarget | null>>());
210-
const lastTargetRef = useRef<CaptureTarget | null>(null);
211216
const targetResolutionRef = useRef<Promise<CaptureTarget | null> | null>(null);
212217
const drainingRef = useRef<Promise<void> | null>(null);
213218
const rerunRequestedRef = useRef(false);
214219
const soundedCaptureIdsRef = useRef(new Set<string>());
215220
const pendingAnimationStartsRef = useRef(new Set<string>());
216221

222+
// Captures go to the last thread or draft on screen, even from routes that show neither.
217223
const currentTarget = routeThreadRef ?? routeDraftId;
218-
if (currentTarget) lastTargetRef.current = currentTarget;
224+
const [lastRouteTarget, setLastRouteTarget] = useState(currentTarget);
225+
if (
226+
currentTarget &&
227+
(lastRouteTarget === null || !isSameCaptureTarget(currentTarget, lastRouteTarget))
228+
) {
229+
setLastRouteTarget(currentTarget);
230+
}
231+
// What a resolution settled on (a promoted draft, a new draft, or nothing) stands in for the
232+
// route's target until the route moves on, so captures in one drain share one new draft.
233+
const settledTargetRef = useRef<{
234+
readonly routeTarget: CaptureTarget | null;
235+
readonly target: CaptureTarget | null;
236+
} | null>(null);
237+
const readLastTarget = useStableCallback(() => {
238+
const settled = settledTargetRef.current;
239+
return settled && settled.routeTarget === lastRouteTarget ? settled.target : lastRouteTarget;
240+
});
241+
const settleLastTarget = useStableCallback((target: CaptureTarget | null) => {
242+
settledTargetRef.current = { routeTarget: lastRouteTarget, target };
243+
});
219244

220-
const resolveTarget = useCallback(async (): Promise<CaptureTarget | null> => {
221-
const lastTarget = lastTargetRef.current;
245+
const resolveTarget = async (): Promise<CaptureTarget | null> => {
246+
const lastTarget = readLastTarget();
222247
if (lastTarget) {
223248
const existingTarget = resolveExistingSnapShotTarget(lastTarget, routeThreadRef);
224249
if (existingTarget) {
225-
lastTargetRef.current = existingTarget;
250+
settleLastTarget(existingTarget);
226251
return existingTarget;
227252
}
228-
lastTargetRef.current = null;
253+
settleLastTarget(null);
229254
}
230255
const projectRef = resolveThreadActionProjectRef({
231256
activeDraftThread,
@@ -236,27 +261,21 @@ export function SnapShotCoordinator() {
236261
if (!projectRef) return null;
237262
const created = await handleNewThread(projectRef);
238263
if (!created) return null;
239-
lastTargetRef.current = created.draftId;
264+
settleLastTarget(created.draftId);
240265
return created.draftId;
241-
}, [activeDraftThread, activeThread, defaultProjectRef, handleNewThread, routeThreadRef]);
266+
};
242267

243-
const resolveCaptureTarget = useCallback(
244-
() => resolveSnapShotTargetOnce(targetResolutionRef, resolveTarget),
245-
[resolveTarget],
246-
);
268+
const resolveCaptureTarget = () => resolveSnapShotTargetOnce(targetResolutionRef, resolveTarget);
247269

248-
const playCaptureSound = useCallback(
249-
(id: string) => {
250-
if (!captureSound || soundedCaptureIdsRef.current.has(id)) return;
251-
soundedCaptureIdsRef.current.add(id);
252-
try {
253-
playSnapShotSound(captureSound);
254-
} catch {}
255-
},
256-
[captureSound],
257-
);
270+
const playCaptureSound = (id: string) => {
271+
if (!captureSound || soundedCaptureIdsRef.current.has(id)) return;
272+
soundedCaptureIdsRef.current.add(id);
273+
try {
274+
playSnapShotSound(captureSound);
275+
} catch {}
276+
};
258277

259-
const drain = useCallback(async () => {
278+
const drain = async () => {
260279
const bridge = getDesktopSnapShotBridge();
261280
if (!bridge) return;
262281
if (drainingRef.current) {
@@ -326,7 +345,7 @@ export function SnapShotCoordinator() {
326345
});
327346
drainingRef.current = operation;
328347
return operation;
329-
}, [playCaptureSound, resolveCaptureTarget, routeThreadRef]);
348+
};
330349

331350
useEffect(() => {
332351
const bridge = getDesktopSnapShotBridge();
@@ -335,7 +354,7 @@ export function SnapShotCoordinator() {
335354
const unsubscribe = bridge.onSnapShotEvent((event) => {
336355
switch (event.type) {
337356
case "requested": {
338-
const current = lastTargetRef.current;
357+
const current = readLastTarget();
339358
const target = current ? resolveExistingSnapShotTarget(current, routeThreadRef) : null;
340359
// Creating a new draft would navigate the renderer before a self-capture finishes.
341360
if (target) {
@@ -386,7 +405,14 @@ export function SnapShotCoordinator() {
386405
}
387406
});
388407
return unsubscribe;
389-
}, [animateCaptures, drain, playCaptureSound, resolveCaptureTarget, routeThreadRef]);
408+
}, [
409+
animateCaptures,
410+
drain,
411+
playCaptureSound,
412+
readLastTarget,
413+
resolveCaptureTarget,
414+
routeThreadRef,
415+
]);
390416

391417
useEffect(() => {
392418
const dismissOnBlur = () => {

0 commit comments

Comments
 (0)