返回 DeepSeek-Reasonix
useDesktopNavigation.ts
根目录 / desktop / frontend / src / app-runtime / useDesktopNavigation.ts
1 import { useLayoutEffect, useRef, type Dispatch, type SetStateAction } from "react";
2 import type { Translator } from "../lib/i18n";
3 import type { useToast } from "../lib/toast";
4 import type { SessionMeta, TabMeta } from "../lib/types";
5 import type { RemoteNavigationCommand } from "../lib/remoteNavigationCommands";
6 import { CommandCancelled, type CommandAuthority, type CommandOutcome } from "../lib/commandOutcome";
7 import { useCommittedAsyncCommand } from "../lib/useCommittedAsyncCommand";
8 import { refreshHistoryProjection, type HistoryViewState } from "./historyViewProjection";
9 import { useCommittedCommand } from "../lib/useCommittedCommand";
10 import { enqueueNavigationRequest, type NavigationCoalescingRefs } from "../lib/openTopicCoalescing";
11 import { useResourceOperations, type SessionResource, type SessionOperationAuthority } from "./useResourceOperations";
12 import type { DesktopNavigationCapture, DesktopNavigationIntent, DesktopNavigationPorts, NavigationNotice } from "./desktopNavigationOwner";
13
14 type QueueInput = { capture: DesktopNavigationCapture; authority: SessionOperationAuthority; result: { error?: unknown; tab?: TabMeta } };
15 let executeNavigation: typeof import("./desktopNavigationOwner").executeDesktopNavigation | undefined;
16 async function runQueuedRequest(request: QueueInput) {
17 try {
18 executeNavigation ??= (await import("./desktopNavigationOwner")).executeDesktopNavigation;
19 request.result.tab = await executeNavigation(request.capture, request.authority);
20 }
21 catch (error) { request.result.error = error; }
22 }
23 async function executeQueuedNavigation(input: { capture: DesktopNavigationCapture; queue: NavigationCoalescingRefs<QueueInput> }, authority: SessionOperationAuthority) {
24 const result: QueueInput["result"] = {};
25 await enqueueNavigationRequest(input.queue, { capture: input.capture, authority, result }, runQueuedRequest);
26 if (result.error) throw result.error;
27 return result.tab;
28 }
29 async function startRemoteNavigation(input: {
30 intent: DesktopNavigationIntent;
31 noteIntent(): number; showChat(): void;
32 execute(intent: DesktopNavigationIntent, seq: number): Promise<CommandOutcome<TabMeta | undefined>>;
33 }, authority: CommandAuthority) {
34 authority.checkpoint();
35 input.showChat();
36 const outcome = await input.execute(input.intent, input.noteIntent());
37 if (outcome.status === "failed") throw outcome.error;
38 if (outcome.status === "cancelled") throw new CommandCancelled(outcome.reason);
39 return outcome.value;
40 }
41
42 /** Owns the existing last-click-wins queue; no App render or view model is queued. */
43 export function useDesktopNavigation(input: {
44 visible: SessionResource;
45 ports: Omit<DesktopNavigationPorts, "reveal" | "projectChanged" | "closeHistory" | "notice" | "applyHistorySessions">;
46 setTabRevealSignal: Dispatch<SetStateAction<number>>;
47 setProjectRevision: Dispatch<SetStateAction<number>>;
48 setHistory: Dispatch<SetStateAction<HistoryViewState | null>>;
49 t: Translator;
50 showToast: ReturnType<typeof useToast>["showToast"];
51 noteIntent(): number;
52 beginSurface(seq: number): void;
53 settleSurface(seq: number): void;
54 showChat(): void;
55 }) {
56 const operations = useResourceOperations({ visible: input.visible });
57 const reveal = useCommittedCommand(() => { input.setTabRevealSignal(value => value + 1); });
58 const projectChanged = useCommittedCommand(() => input.setProjectRevision(value => value + 1));
59 const closeHistory = useCommittedCommand(() => input.setHistory(null));
60 const applyHistorySessions = useCommittedCommand((sessions: SessionMeta[]) => input.setHistory(current => refreshHistoryProjection(current, sessions)));
61 const notice = useCommittedCommand((notice: NavigationNotice) => {
62 input.showToast("key" in notice ? input.t(notice.key, notice.params) : notice.message, notice.tone, { durationMs: notice.durationMs });
63 });
64 const queueRef = useRef<NavigationCoalescingRefs<QueueInput> | null>(null);
65 if (!queueRef.current) queueRef.current = { seqRef: { current: 0 }, runningRef: { current: false }, pendingRef: { current: null } };
66 const queue = queueRef.current;
67 const settle = useCommittedCommand(input.settleSurface);
68 const executeWithIntent = useCommittedCommand(async (intent: DesktopNavigationIntent, navigationIntentSeq: number) => {
69 input.beginSurface(navigationIntentSeq);
70 try {
71 return await operations({ kind: "application" }, "navigation", {
72 queue, capture: { intent, navigationIntentSeq,
73 ports: { ...input.ports, reveal, projectChanged, closeHistory, notice, applyHistorySessions } },
74 }, executeQueuedNavigation);
75 } finally { settle(navigationIntentSeq); }
76 });
77 const enqueueNavigationWithIntent = useCommittedCommand(async (intent: DesktopNavigationIntent, seq: number): Promise<void> => { await executeWithIntent(intent, seq); });
78 const enqueueNavigation = useCommittedCommand((intent: DesktopNavigationIntent) => {
79 input.showChat();
80 return enqueueNavigationWithIntent(intent, input.noteIntent());
81 });
82 const openRemoteProject: RemoteNavigationCommand = useCommittedAsyncCommand(
83 (...[remote, options]: Parameters<RemoteNavigationCommand>) => ({
84 intent: { kind: "remote-project", remote: { ...remote }, options: { ...options } } as DesktopNavigationIntent,
85 showChat: input.showChat, noteIntent: input.noteIntent, execute: executeWithIntent,
86 }), startRemoteNavigation);
87 useLayoutEffect(() => () => {
88 queue.seqRef.current++;
89 queue.pendingRef.current?.resolve();
90 queue.pendingRef.current = null;
91 }, [queue]);
92 return { enqueueNavigation, enqueueNavigationWithIntent, openRemoteProject };
93 }
94
94 lines TYPESCRIPT