@@ -26,6 +26,7 @@ import { useDepositNetworkDepositoryCount } from "./hooks/use-deposit-network-de
2626import { useDepositUrlNavigation } from "./hooks/use-deposit-url-navigation" ;
2727import { useDepositOptionActions } from "./hooks/use-deposit-option-actions" ;
2828import { useDepositActivityRecording } from "./hooks/use-deposit-activity-recording" ;
29+ import { useDepositSynthesisLifecycle } from "./hooks/use-deposit-synthesis-lifecycle" ;
2930import { DepositRouteStateAside } from "@/components/deposits/DepositRouteStateAside/DepositRouteStateAside" ;
3031import { DepositPipelinesMaster } from "@/components/deposits/DepositPipelinesMaster/DepositPipelinesMaster" ;
3132import { DepositSynthesisTelemetry } from "@/components/deposits/DepositSynthesisTelemetry/DepositSynthesisTelemetry" ;
@@ -54,7 +55,6 @@ import {
5455import { BitcodeShellBridgeProvider } from "@/components/bitcode/layout/BitcodeShellBridge/BitcodeShellBridge" ;
5556import {
5657 buildDepositRouteSession ,
57- writeDepositRouteStage ,
5858} from "@/components/deposits/models/deposit-route-model" ;
5959import {
6060 DEPOSIT_HEADER_METRIC_EXPLAINERS ,
@@ -81,10 +81,6 @@ import {
8181import { buildDepositSourceCriticalitySignals } from "@/components/deposits/models/deposit-source-criticality" ;
8282import { buildDepositRouteInput } from "@/components/deposits/models/deposit-route-input-builder" ;
8383import { resolvePreferredSignerAddress } from "@/components/deposits/models/deposit-preferred-signer" ;
84- import {
85- adoptSelectionStatusFromRun ,
86- synthesisStatusFromRunRow ,
87- } from "@/components/deposits/models/deposit-run-status" ;
8884
8985export default function DepositPageClient ( ) {
9086 const { user } = useAuth ( ) ;
@@ -179,7 +175,6 @@ export default function DepositPageClient() {
179175
180176 const synthesizeOptionsRef = useRef < ( ( ) => Promise < void > ) | null > ( null ) ;
181177 const synthesisTelemetryRef = useRef < HTMLElement | null > ( null ) ;
182- const lastAdoptedSelectionIdRef = useRef < string | null > ( null ) ;
183178 const lastTrackedSourceRef = useRef < string | null > ( null ) ;
184179
185180 const closePipelineDetail = useCallback ( ( ) => {
@@ -356,178 +351,43 @@ export default function DepositPageClient() {
356351 refreshLiveRuns,
357352 } ) ;
358353
359- // Resume synthesized options when a dispatched/adopted run completes.
360- useEffect ( ( ) => {
361- if (
362- ( synthesisStatus !== "running" && synthesisStatus !== "complete" ) ||
363- ! synthesisRunId ||
364- realSynthesis
365- ) {
366- return ;
367- }
368- if (
369- synthesisActivity . error &&
370- ( synthesisExecutionMatchesRun || synthesisStreamError )
371- ) {
372- setSynthesisStatus ( "failed" ) ;
373- setSynthesisError ( synthesisActivity . error ) ;
374- if ( synthesisDispatchedAtMs !== null ) {
375- trackProductEvent ( {
376- name : "deposit_synthesis_failed" ,
377- data : {
378- stage : "run" ,
379- durationMs : Date . now ( ) - synthesisDispatchedAtMs ,
380- } ,
381- } ) ;
382- }
383- return ;
384- }
385- if ( ! synthesisExecutionMatchesRun ) return ;
386- const rowCompleted =
387- String (
388- ( synthesisExecution as { status ?: string } | null ) ?. status || "" ,
389- ) . toLowerCase ( ) === "completed" ;
390- if ( ! synthesisActivity . isStreamingComplete && ! rowCompleted ) return ;
391- if ( ! synthesisRunExpectsOptions ) {
392- setSynthesisStatus ( "complete" ) ;
393- return ;
394- }
395- let cancelled = false ;
396- void ( async ( ) => {
397- try {
398- const res = await fetch ( `/api/executions/history/${ synthesisRunId } ` ) ;
399- const data = await res . json ( ) . catch ( ( ) => null ) ;
400- const output = data ?. run ?. output as
401- | { depositOptionSynthesis ?: unknown ; reviewProjections ?: unknown }
402- | undefined ;
403- const synthesis = output ?. depositOptionSynthesis ;
404- if ( ! res . ok || ! synthesis ) {
405- throw new Error ( "Synthesized options were not found for this run." ) ;
406- }
407- if ( cancelled ) return ;
408- setRealSynthesis ( {
409- synthesis : synthesis as NonNullable < typeof realSynthesis > [ "synthesis" ] ,
410- reviewProjections : Array . isArray ( output ?. reviewProjections )
411- ? ( output ! . reviewProjections as NonNullable <
412- typeof realSynthesis
413- > [ "reviewProjections" ] )
414- : [ ] ,
415- } ) ;
416- setOptionsRequested ( true ) ;
417- setSynthesisStatus ( "complete" ) ;
418- if ( synthesisDispatchedAtMs !== null ) {
419- const options = ( synthesis as { options ?: unknown [ ] } ) . options ;
420- trackProductEvent ( {
421- name : "deposit_synthesis_completed" ,
422- data : {
423- optionCount : Array . isArray ( options ) ? options . length : 0 ,
424- durationMs : Date . now ( ) - synthesisDispatchedAtMs ,
425- } ,
426- } ) ;
427- }
428- replaceDepositSearchParams (
429- writeDepositRouteStage ( readCurrentSearchParams ( ) , "review-options" ) ,
430- ) ;
431- void refreshLiveRuns ( ) ;
432- } catch ( error ) {
433- if ( cancelled ) return ;
434- setSynthesisStatus ( "failed" ) ;
435- setSynthesisError (
436- error instanceof Error
437- ? error . message
438- : "Synthesis result not found." ,
439- ) ;
440- if ( synthesisDispatchedAtMs !== null ) {
441- trackProductEvent ( {
442- name : "deposit_synthesis_failed" ,
443- data : {
444- stage : "resume" ,
445- durationMs : Date . now ( ) - synthesisDispatchedAtMs ,
446- } ,
447- } ) ;
448- }
449- }
450- } ) ( ) ;
451- return ( ) => {
452- cancelled = true ;
453- } ;
454- } , [
354+ const { handleSynthesizeOptions } = useDepositSynthesisLifecycle ( {
455355 synthesisStatus,
356+ setSynthesisStatus,
456357 synthesisRunId,
457- synthesisRunExpectsOptions ,
358+ setSynthesisRunId ,
458359 realSynthesis,
459- synthesisActivity . isStreamingComplete ,
460- synthesisActivity . error ,
461- synthesisDispatchedAtMs ,
462- synthesisExecution ,
360+ setRealSynthesis,
361+ synthesisActivity,
463362 synthesisExecutionMatchesRun,
464363 synthesisStreamError,
364+ synthesisExecution,
365+ synthesisDispatchedAtMs,
366+ setSynthesisDispatchedAtMs,
367+ setSynthesisError,
368+ synthesisRunExpectsOptions,
369+ setSynthesisRunExpectsOptions,
370+ setOptionsRequested,
371+ setSynthesisLogScrolled,
372+ liveRuns,
373+ selectedRun,
465374 readCurrentSearchParams,
466- refreshLiveRuns ,
467375 replaceDepositSearchParams,
468- ] ) ;
469-
470- // Row-status reconciliation when SSE is quiet but the row is terminal.
471- useEffect ( ( ) => {
472- if ( synthesisStatus !== "running" || ! synthesisRunId ) return ;
473- const run = liveRuns . find ( ( candidate ) => candidate . id === synthesisRunId ) ;
474- if ( ! run ) return ;
475- const mapped = synthesisStatusFromRunRow ( run ) ;
476- if ( mapped . status === "running" ) return ;
477- setSynthesisStatus ( mapped . status ) ;
478- setSynthesisError ( mapped . error ) ;
479- if ( synthesisDispatchedAtMs === null ) return ;
480- if ( mapped . status === "cancelled" ) {
481- trackProductEvent ( {
482- name : "deposit_synthesis_cancelled" ,
483- data : { durationMs : Date . now ( ) - synthesisDispatchedAtMs } ,
484- } ) ;
485- } else if ( mapped . status === "failed" ) {
486- trackProductEvent ( {
487- name : "deposit_synthesis_failed" ,
488- data : {
489- stage : "run" ,
490- durationMs : Date . now ( ) - synthesisDispatchedAtMs ,
491- } ,
492- } ) ;
493- }
494- } , [ liveRuns , synthesisDispatchedAtMs , synthesisRunId , synthesisStatus ] ) ;
495-
496- useEffect ( ( ) => {
497- if ( synthesisStatus !== "running" || ! synthesisRunId ) return ;
498- const interval = window . setInterval ( ( ) => {
499- void refreshLiveRuns ( ) ;
500- } , 15_000 ) ;
501- return ( ) => window . clearInterval ( interval ) ;
502- } , [ refreshLiveRuns , synthesisRunId , synthesisStatus ] ) ;
503-
504- // Master-detail adoption from URL selection.
505- useEffect ( ( ) => {
506- const run = selectedRun ;
507- if ( ! run ?. id ) {
508- lastAdoptedSelectionIdRef . current = null ;
509- return ;
510- }
511- if ( lastAdoptedSelectionIdRef . current === run . id ) return ;
512- lastAdoptedSelectionIdRef . current = run . id ;
513- if ( run . id === synthesisRunId ) return ;
514- if ( synthesisDispatchedAtMs !== null && synthesisStatus === "running" ) {
515- return ;
516- }
517- setSynthesisRunId ( run . id ) ;
518- setSynthesisRunExpectsOptions (
519- run . contextSource === "deposit-option-synthesis" ,
520- ) ;
521- setSynthesisDispatchedAtMs ( null ) ;
522- setSynthesisLogScrolled ( false ) ;
523- setRealSynthesis ( null ) ;
524- setSynthesisError ( null ) ;
525- setOptionsRequested ( false ) ;
526- const mapped = adoptSelectionStatusFromRun ( run ) ;
527- setSynthesisStatus ( mapped . status ) ;
528- setSynthesisError ( mapped . error ) ;
529- } , [ selectedRun , synthesisRunId , synthesisDispatchedAtMs , synthesisStatus ] ) ;
376+ replaceDepositRouteTransaction,
377+ refreshLiveRuns,
378+ obfuscations,
379+ forcedInclusions,
380+ forcedExclusions,
381+ repositoryContext,
382+ depositoryDemandSignals : depositRouteInput . depositoryDemandSignals ,
383+ readingDemandSignals : depositRouteInput . readingDemandSignals ,
384+ existingDepositorySignals : depositRouteInput . existingDepositorySignals ,
385+ synthesizeOptionsRef,
386+ synthesisTelemetryRef,
387+ } ) ;
530388
389+ // Funnel analytics: one source-safe event per distinct repository selection
390+ // per mount — provider + pin shape only, never the repository name.
531391 useEffect ( ( ) => {
532392 const fullName = repositoryContext ?. selectedRepository ?. fullName || null ;
533393 if ( ! fullName || lastTrackedSourceRef . current === fullName ) return ;
@@ -571,111 +431,6 @@ export default function DepositPageClient() {
571431 setIsObfuscationsAnchorPopoverOpen,
572432 } ) ;
573433
574- const handleSynthesizeOptions = useCallback (
575- async ( instructionsOverride ?: string ) => {
576- const effectiveInstructions =
577- typeof instructionsOverride === "string" && instructionsOverride . trim ( )
578- ? instructionsOverride
579- : obfuscations ;
580- setSynthesisStatus ( "running" ) ;
581- setSynthesisError ( null ) ;
582- setRealSynthesis ( null ) ;
583- const runId =
584- typeof crypto !== "undefined" && "randomUUID" in crypto
585- ? crypto . randomUUID ( )
586- : `${ Date . now ( ) } -${ Math . random ( ) . toString ( 16 ) . slice ( 2 ) } ` ;
587- setSynthesisRunId ( runId ) ;
588- setSynthesisRunExpectsOptions ( true ) ;
589- setSynthesisDispatchedAtMs ( Date . now ( ) ) ;
590- setSynthesisLogScrolled ( false ) ;
591-
592- try {
593- const response = await fetch ( "/api/deposit/synthesize-options" , {
594- method : "POST" ,
595- headers : { "Content-Type" : "application/json" } ,
596- body : JSON . stringify ( {
597- runId,
598- repositoryFullName :
599- repositoryContext ?. selectedRepository ?. fullName || null ,
600- sourceBranch : repositoryContext ?. selectedBranch || null ,
601- sourceCommit : repositoryContext ?. selectedCommit || null ,
602- obfuscations : effectiveInstructions ,
603- forcedInclusions,
604- forcedExclusions,
605- demandContext : [
606- ...depositRouteInput . depositoryDemandSignals . map (
607- ( signal ) => signal . label ,
608- ) ,
609- ...depositRouteInput . readingDemandSignals . map (
610- ( signal ) => signal . label ,
611- ) ,
612- ] ,
613- depositoryDemandSignals : depositRouteInput . depositoryDemandSignals ,
614- readingDemandSignals : depositRouteInput . readingDemandSignals ,
615- existingDepositorySignals :
616- depositRouteInput . existingDepositorySignals ,
617- } ) ,
618- } ) ;
619- const payload = await response . json ( ) . catch ( ( ) => null ) ;
620- if ( ! response . ok || ! payload ?. ok ) {
621- throw new Error (
622- typeof payload ?. error === "string"
623- ? payload . error
624- : "Deposit option synthesis failed." ,
625- ) ;
626- }
627- trackProductEvent ( {
628- name : "deposit_synthesis_dispatched" ,
629- data : {
630- hasObfuscations : Boolean ( effectiveInstructions . trim ( ) ) ,
631- forcedInclusionCount : forcedInclusions . length ,
632- forcedExclusionCount : forcedExclusions . length ,
633- demandSignalCount :
634- depositRouteInput . depositoryDemandSignals . length +
635- depositRouteInput . readingDemandSignals . length ,
636- } ,
637- } ) ;
638- void refreshLiveRuns ( ) . then ( ( ) => {
639- replaceDepositRouteTransaction ( runId ) ;
640- } ) ;
641- } catch ( error ) {
642- setSynthesisStatus ( "failed" ) ;
643- setSynthesisError (
644- error instanceof Error
645- ? error . message
646- : "Deposit option synthesis failed." ,
647- ) ;
648- trackProductEvent ( {
649- name : "deposit_synthesis_failed" ,
650- data : { stage : "dispatch" , durationMs : null } ,
651- } ) ;
652- }
653- } ,
654- [
655- obfuscations ,
656- depositRouteInput . depositoryDemandSignals ,
657- depositRouteInput . existingDepositorySignals ,
658- depositRouteInput . readingDemandSignals ,
659- forcedExclusions ,
660- refreshLiveRuns ,
661- replaceDepositRouteTransaction ,
662- repositoryContext ,
663- forcedInclusions ,
664- ] ,
665- ) ;
666-
667- useEffect ( ( ) => {
668- synthesizeOptionsRef . current = handleSynthesizeOptions ;
669- } , [ handleSynthesizeOptions ] ) ;
670-
671- useEffect ( ( ) => {
672- if ( ! synthesisRunId || synthesisDispatchedAtMs === null ) return ;
673- synthesisTelemetryRef . current ?. scrollIntoView ?.( {
674- behavior : "smooth" ,
675- block : "start" ,
676- } ) ;
677- } , [ synthesisRunId , synthesisDispatchedAtMs ] ) ;
678-
679434 const {
680435 handleOptionReviewDecision,
681436 handleToggleSelect,
0 commit comments