| 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 |