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