返回 DeepSeek-Reasonix
useNavigationSurface.ts
根目录 / desktop / frontend / src / lib / useNavigationSurface.ts
1 import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2 import { flushSync } from "react-dom";
3 import type { Item } from "./useController";
4 import { recordFrontendDiagnostic } from "./frontendDiagnosticBridge";
5 import { noteNavigationFirstPaint, noteNavigationRequested } from "./sessionDiagnostics";
6 import {
7 beginNavigationSurfaceState,
8 createNavigationSurfaceTicket,
9 markNavigationTargetMasked,
10 matchesNavigationSurfaceTicket,
11 settleNavigationSurfaceState,
12 type NavigationSurfaceTicket,
13 type NavigationSurfaceState,
14 } from "./navigationSurfaceTransition";
15 import { useCommittedCommand } from "./useCommittedCommand";
16
17 export type PreservedTranscriptSurface = {
18 tabId?: string;
19 items: Item[];
20 geometrySessionKey?: string;
21 };
22
23 export function useNavigationSurface(target: {
24 activeTabId?: string;
25 sessionKey: string;
26 ready: boolean;
27 backendActivationPending: boolean;
28 hydrating: boolean;
29 hydrateError?: string;
30 }) {
31 const [surface, setSurface] = useState<NavigationSurfaceState>(null);
32 const [preserved, setPreserved] = useState<PreservedTranscriptSurface | null>(null);
33 const renderedRef = useRef<PreservedTranscriptSurface | null>(null);
34 const intent = surface?.intent ?? null;
35 const transitioning = intent !== null;
36 const dataReady = Boolean(
37 surface?.phase === "target-masked" && target.activeTabId && target.ready &&
38 !target.backendActivationPending && !target.hydrating && !target.hydrateError,
39 );
40 const failed = Boolean(
41 surface?.phase === "target-masked" && target.activeTabId &&
42 !target.backendActivationPending && !target.hydrating && target.hydrateError,
43 );
44
45 const begin = useCommittedCommand((nextIntent: number) => {
46 noteNavigationRequested(nextIntent);
47 recordFrontendDiagnostic("navigation", "navigation.begin", { intent: nextIntent, phase: "begin" });
48 const rendered = renderedRef.current;
49 flushSync(() => {
50 setPreserved(rendered?.items.length ? rendered : null);
51 setSurface(beginNavigationSurfaceState(nextIntent));
52 });
53 renderedRef.current = null;
54 });
55 const maskTarget = useCommittedCommand((completedIntent: number) => {
56 setSurface((current) => markNavigationTargetMasked(current, completedIntent));
57 });
58 const settle = useCommittedCommand((completedIntent: number, outcome: "ready" | "degraded" | "failed") => {
59 if (outcome !== "failed") {
60 noteNavigationFirstPaint(completedIntent);
61 recordFrontendDiagnostic("navigation", "navigation.paint-ready", { intent: completedIntent, outcome });
62 }
63 recordFrontendDiagnostic("navigation", "navigation.terminal", { intent: completedIntent, outcome });
64 recordFrontendDiagnostic("navigation", "navigation.settle", {
65 intent: completedIntent,
66 phase: outcome === "failed" ? "data-failed" : "paint-ready",
67 outcome,
68 });
69 setSurface((current) => settleNavigationSurfaceState(current, completedIntent));
70 setPreserved(null);
71 });
72 const ticket = useMemo<NavigationSurfaceTicket | null>(() => {
73 if (!dataReady || intent === null || !target.activeTabId) return null;
74 return createNavigationSurfaceTicket(intent, target.activeTabId, target.sessionKey);
75 }, [dataReady, intent, target.activeTabId, target.sessionKey]);
76 const committedTicketRef = useRef<NavigationSurfaceTicket | null>(null);
77 useLayoutEffect(() => {
78 committedTicketRef.current = ticket;
79 }, [ticket]);
80 useLayoutEffect(() => () => {
81 committedTicketRef.current = null;
82 renderedRef.current = null;
83 }, []);
84 const commitPaint = useCommittedCommand((token: string, outcome: "ready" | "degraded") => {
85 const committedTicket = committedTicketRef.current;
86 if (!matchesNavigationSurfaceTicket(
87 committedTicket,
88 token,
89 surface?.intent ?? null,
90 target.activeTabId,
91 target.sessionKey,
92 )) return null;
93 committedTicketRef.current = null;
94 settle(committedTicket!.intent, outcome);
95 return committedTicket;
96 });
97 const commitRendered = useCommittedCommand((rendered: PreservedTranscriptSurface | null) => {
98 renderedRef.current = rendered;
99 });
100
101 const dataReadyIntentRef = useRef<number | null>(null);
102 useEffect(() => {
103 if (!dataReady || intent === null || dataReadyIntentRef.current === intent) return;
104 dataReadyIntentRef.current = intent;
105 recordFrontendDiagnostic("navigation", "navigation.target-mounted", { intent });
106 recordFrontendDiagnostic("navigation", "navigation.data-ready", { intent, outcome: "ready" });
107 }, [dataReady, intent]);
108 useEffect(() => {
109 if (!failed || intent === null) return;
110 recordFrontendDiagnostic("navigation", "navigation.data-ready", { intent, outcome: "failed" });
111 settle(intent, "failed");
112 }, [failed, intent, settle]);
113 useEffect(() => {
114 if (surface === null) setPreserved(null);
115 }, [surface]);
116
117 return {
118 surface,
119 intent,
120 transitioning,
121 dataReady,
122 preserved,
123 surfaceCommitToken: ticket?.token,
124 commitRendered,
125 begin,
126 maskTarget,
127 commitPaint,
128 };
129 }
130
130 lines TYPESCRIPT