返回 DeepSeek-Reasonix
useTranscriptSurfaceProjection.ts
根目录 / desktop / frontend / src / app-runtime / useTranscriptSurfaceProjection.ts
1 import { useLayoutEffect } from "react";
2 import { useCommittedCommand } from "../lib/useCommittedCommand";
3 import type { useNavigationSurface } from "../lib/useNavigationSurface";
4 import type { HistoryLoadOutcome, HistoryLoadTrigger, Item, State } from "../lib/useController";
5 import type { SessionAvailability } from "../lib/sessionAvailability";
6 import type { NavigateToTurn, TurnNavigationTarget } from "../lib/historyTurnNavigation";
7
8 type NavigationSurfaceApi = ReturnType<typeof useNavigationSurface>;
9
10 export type TranscriptSurfaceProjectionInput = {
11 hydrating: boolean;
12 hydrateHistoryLoaded: boolean | undefined;
13 hydratePlaceholderItems: Item[] | undefined;
14 hydratePlaceholderActive: boolean;
15 items: Item[];
16 guidanceConsumed?: State["guidanceConsumed"];
17 remote: boolean;
18 remoteItems: Item[];
19 activeTabId: string | undefined;
20 geometrySessionKey: string;
21 transitioning: boolean;
22 navigationDataReady: boolean;
23 preserved: NavigationSurfaceApi["preserved"];
24 controllerReady: boolean;
25 availability: SessionAvailability;
26 sessionActivity: boolean;
27 imDetailActive: boolean;
28 sessionHasContent: boolean;
29 commitRendered: NavigationSurfaceApi["commitRendered"];
30 commitPaint: NavigationSurfaceApi["commitPaint"];
31 commitSingleSurface: (tabId: string) => void;
32 ports: {
33 navigateToTurn?(tabId: string, target: TurnNavigationTarget, current: () => boolean): ReturnType<NavigateToTurn>;
34 loadOlderHistory(tabId: string, targetTurn: number | undefined, trigger: HistoryLoadTrigger): Promise<HistoryLoadOutcome>;
35 loadNewerHistory(tabId: string, latest: boolean, current?: () => boolean): Promise<HistoryLoadOutcome>;
36 commitThenSend(tabId: string, displayText: string, submitText?: string): Promise<void>;
37 };
38 };
39
40 /**
41 * Owns the transcript surface projection: hydration placeholders, the
42 * empty-session hero gate, the committed-surface commit effect (only
43 * committed presentation may become a retained source), the visible
44 * source-retained surface selection, surface paint receipts, the latest
45 * consumed guidance entry and the transcript prompt/load-older commands.
46 */
47 export function useTranscriptSurfaceProjection(input: TranscriptSurfaceProjectionInput) {
48 const { activeTabId, transitioning, ports } = input;
49 const transcriptHydrating = input.hydrating && !input.hydrateHistoryLoaded;
50 // Show the hero only after history hydration settles on a truly empty session.
51 // Avoid flash while switching tabs: items may be empty while placeholders show.
52 // Exclude IM/Bot detail: hero CSS collapses .main, which also hosts that panel.
53 const emptyHero =
54 input.availability.kind === "ready" &&
55 !input.sessionActivity &&
56 !transitioning &&
57 !input.imDetailActive &&
58 !input.sessionHasContent &&
59 !transcriptHydrating &&
60 !input.hydratePlaceholderActive;
61 const transcriptItems = input.hydratePlaceholderActive ? input.hydratePlaceholderItems! : input.items;
62 const handleLoadOlderHistory = useCommittedCommand((targetTurn?: number, trigger: HistoryLoadTrigger = "retry") => {
63 return activeTabId ? ports.loadOlderHistory(activeTabId, targetTurn, trigger) : Promise.resolve("empty" as const);
64 });
65 const handleLoadNewerHistory = useCommittedCommand((latest = false, current?: () => boolean) => {
66 return activeTabId ? ports.loadNewerHistory(activeTabId, latest, current) : Promise.resolve("empty" as const);
67 });
68 const handleNavigateToTurn = useCommittedCommand((target: TurnNavigationTarget, current: () => boolean) => {
69 return activeTabId && !transitioning && ports.navigateToTurn ? ports.navigateToTurn(activeTabId, target, current) : Promise.resolve("cancelled" as const);
70 });
71
72 // Display items: backend history is authoritative after immediate commit.
73 // rewindState only drives the undo banner, not optimistic truncation.
74 const displayItems = transcriptItems;
75 const committedSurfaceItems = input.remote ? input.remoteItems : displayItems;
76 const committedGeometryKey = input.remote ? `tab:${activeTabId ?? "preview"}` : input.geometrySessionKey;
77 // Only committed presentation can become a future retained source surface.
78 // A suspended or abandoned render must never become navigation authority.
79 const commitRendered = input.commitRendered;
80 useLayoutEffect(() => {
81 if (transitioning) return;
82 commitRendered({
83 tabId: activeTabId,
84 items: committedSurfaceItems,
85 geometrySessionKey: committedGeometryKey,
86 });
87 }, [activeTabId, commitRendered, committedSurfaceItems, transitioning, committedGeometryKey]);
88 const visibleTranscriptSurface = transitioning && !input.navigationDataReady && input.preserved
89 ? input.preserved
90 : null;
91 const visibleTranscriptItems = visibleTranscriptSurface?.items ?? displayItems;
92 const visibleTranscriptTabId = visibleTranscriptSurface?.tabId ?? activeTabId;
93 const visibleTranscriptGeometryKey = visibleTranscriptSurface?.geometrySessionKey ?? input.geometrySessionKey;
94 const handleSurfacePaintReady = useCommittedCommand((token: string, outcome: "ready" | "degraded") => {
95 const receipt = input.commitPaint(token, outcome);
96 if (receipt) input.commitSingleSurface(receipt.targetTabId);
97 });
98 const latestGuidanceConsumed = input.guidanceConsumed ?? null;
99
100 const handleTranscriptPrompt = useCommittedCommand((text: string, submitText = text) => {
101 if (!activeTabId || !input.controllerReady) return;
102 void ports.commitThenSend(activeTabId, text, submitText).catch((err) => {
103 console.warn("Failed to submit transcript prompt", err);
104 });
105 });
106
107 return {
108 transcriptHydrating,
109 emptyHero,
110 visibleTranscriptItems,
111 visibleTranscriptTabId,
112 visibleTranscriptGeometryKey,
113 handleLoadOlderHistory,
114 handleLoadNewerHistory,
115 handleNavigateToTurn,
116 handleSurfacePaintReady,
117 latestGuidanceConsumed,
118 handleTranscriptPrompt,
119 };
120 }
121
121 lines TYPESCRIPT