| 1 | import { browserMockScenarioParam, GUIDANCE_QUEUE_MOCK_ITEMS, isGuidanceMockScenario } from "../lib/mockScenarios"; |
| 2 | import { desktopHost } from "../lib/desktopHost"; |
| 3 | import { formatShortcutCombo, resolvedShortcutCombo } from "../lib/keyboardShortcuts"; |
| 4 | import { showWorktreeCleanupNotice } from "../lib/worktreeCleanupNotice"; |
| 5 | import { desktopBridge } from "./desktopBridgeAdapter"; |
| 6 | import { desktopProjectAdapter } from "./desktopProjectAdapter"; |
| 7 | import { useHistoryCommands } from "./useHistoryCommands"; |
| 8 | import { useSessionNavigationCommands } from "./useSessionNavigationCommands"; |
| 9 | import { usePaletteCommands } from "./usePaletteCommands"; |
| 10 | import { useTopicNavigationShortcuts } from "./useTopicNavigationShortcuts"; |
| 11 | import { useProjectTopicCommands } from "./useProjectTopicCommands"; |
| 12 | import { useAppChromeCommands } from "./useAppChromeCommands"; |
| 13 | import { useOnboardingCommands } from "./useOnboardingCommands"; |
| 14 | import { useWorktreeMergeCommands } from "./useWorktreeMergeCommands"; |
| 15 | import type { HistoryViewState } from "./historyViewProjection"; |
| 16 | import type { State } from "../lib/useController"; |
| 17 | import type { TabMeta } from "../lib/types"; |
| 18 | import type { Translator } from "../lib/i18n"; |
| 19 | import type { useAppRuntimeAdapter } from "./useAppRuntimeAdapter"; |
| 20 | import type { useAppShellStores } from "./useAppShellStores"; |
| 21 | import type { useNavigationSurface } from "../lib/useNavigationSurface"; |
| 22 | import type { useAppSessionComposition } from "./useAppSessionComposition"; |
| 23 | import type { useSessionDraftSurface } from "./useSessionDraftSurface"; |
| 24 | |
| 25 | type Runtime = ReturnType<typeof useAppRuntimeAdapter>; |
| 26 | type Shell = ReturnType<typeof useAppShellStores>; |
| 27 | type SessionComposition = ReturnType<typeof useAppSessionComposition>; |
| 28 | |
| 29 | export type AppNavigationCompositionInput = { |
| 30 | runtime: Runtime; |
| 31 | t: Translator; |
| 32 | notice: Runtime["snapshot"]["notice"]; |
| 33 | showToast: (message: string, level?: "info" | "warn" | "error", options?: { durationMs?: number }) => void; |
| 34 | shell: Shell; |
| 35 | state: State; |
| 36 | activeTab: TabMeta | undefined; |
| 37 | activeTabId: string | undefined; |
| 38 | activeSessionIdentity: string; |
| 39 | remoteSurfaceActive: boolean; |
| 40 | surface: Pick<ReturnType<typeof useNavigationSurface>, "begin" | "maskTarget">; |
| 41 | local: { |
| 42 | setHistView: React.Dispatch<React.SetStateAction<HistoryViewState | null>>; |
| 43 | setProjectRevision: React.Dispatch<React.SetStateAction<number>>; |
| 44 | setSidebarImDetailConnectionId: React.Dispatch<React.SetStateAction<string>>; |
| 45 | setTasksOpen: React.Dispatch<React.SetStateAction<false | "session" | "all">>; |
| 46 | }; |
| 47 | session: SessionComposition; |
| 48 | draft: ReturnType<typeof useSessionDraftSurface>; |
| 49 | }; |
| 50 | |
| 51 | /** |
| 52 | * Navigation/chrome composition: history, automation, desktop and session |
| 53 | * navigation, command palette, topic shortcuts, project/topic commands, |
| 54 | * window chrome and worktree merge. Runs after the session composition in |
| 55 | * the App body's original order; pure relocation. |
| 56 | */ |
| 57 | export function useAppNavigationComposition(input: AppNavigationCompositionInput) { |
| 58 | const { runtime, t, notice, showToast, shell, state, activeTab, activeTabId, activeSessionIdentity, session } = input; |
| 59 | const { remoteSurfaceActive } = input; |
| 60 | const { begin: beginNavigationSurface, maskTarget: settleNavigationSurface } = input.surface; |
| 61 | const { |
| 62 | setHistView, setProjectRevision, |
| 63 | setSidebarImDetailConnectionId, setTasksOpen, |
| 64 | } = input.local; |
| 65 | const { |
| 66 | noteNavigationIntent, registeredNavigationIntent, |
| 67 | isNavigationIntentCurrent, syncActiveTab, ensureBlankSurface, |
| 68 | } = runtime.navigation; |
| 69 | const { listSessions, deleteSession, renameSession } = runtime.sessionActions; |
| 70 | const { refreshMeta, pickWorkspace, switchWorkspace } = runtime.workspace; |
| 71 | const { |
| 72 | managementActive, desktopPlatform, windowsFramelessChrome, sidebarCollapsed, |
| 73 | openPage, returnToWorkspace, enterConversation, |
| 74 | setSettingsTarget, setSettingsFocus, setSidebarSearchOpen, setSidebarSearchFocusSignal, setProviderSetupNeeded, |
| 75 | } = shell; |
| 76 | const { reload: reloadDesktopPreferences } = shell.preferences; |
| 77 | const { |
| 78 | closeTransientOverlays, refreshProviderSetupState, |
| 79 | tabBarCommands, terminalPanelCommands, remoteWorkspaceCommands, runtimeEventCommands, |
| 80 | desktopNavigation: desktopNavigationBag, |
| 81 | } = session; |
| 82 | const { refreshTabMetas, seedActiveTabMeta } = runtimeEventCommands; |
| 83 | const { handleTabClose } = tabBarCommands; |
| 84 | const { toggleTerminalPanel } = terminalPanelCommands; |
| 85 | const { openRemoteWorkspaceFromStatus, connectAndOpenRemoteWorkspace } = remoteWorkspaceCommands; |
| 86 | const { enqueueNavigation, enqueueNavigationWithIntent, openRemoteProject } = desktopNavigationBag; |
| 87 | const { toggleSidebar } = session.shellGeometry; |
| 88 | |
| 89 | const historyCommands = useHistoryCommands({ |
| 90 | running: state.running, |
| 91 | setHistView, |
| 92 | ports: { |
| 93 | listSessions, |
| 94 | deleteSession, |
| 95 | renameSession, |
| 96 | openPage: (page) => openPage(page), |
| 97 | }, |
| 98 | }); |
| 99 | const { openTrash, refreshHistoryView } = historyCommands; |
| 100 | |
| 101 | |
| 102 | const navigationCommands = useSessionNavigationCommands({ |
| 103 | activeTab, |
| 104 | showToast, |
| 105 | closeTransientOverlays, |
| 106 | clearImDetail: () => setSidebarImDetailConnectionId(""), |
| 107 | prepareBlankWorkspace: (workspaceRoot) => { |
| 108 | session.workspacePanelCommands.prepareBlankWorkspace(workspaceRoot); |
| 109 | }, |
| 110 | navigation: { enqueueNavigation, enqueueNavigationWithIntent, openRemoteProject }, |
| 111 | noteNavigationIntent, |
| 112 | beginNavigationSurface, |
| 113 | settleNavigationSurface, |
| 114 | isNavigationIntentCurrent, |
| 115 | markProjectChanged: setProjectRevision, |
| 116 | refreshTabMetas, |
| 117 | refreshHistoryView, |
| 118 | enterConversation, |
| 119 | pickWorkspace, |
| 120 | switchWorkspace, |
| 121 | draft: { |
| 122 | target: input.draft.surface ? { scope: input.draft.surface.draft.scope, workspaceRoot: input.draft.surface.draft.workspaceRoot } : undefined, |
| 123 | open: input.draft.open, |
| 124 | dismiss: input.draft.dismiss, |
| 125 | }, |
| 126 | ports: { |
| 127 | openTaskSessionForTab: (tabId, taskId) => desktopBridge.openTaskSessionForTab(tabId, taskId), |
| 128 | listSessionsForTab: (tabId) => desktopBridge.listSessionsForTab(tabId), |
| 129 | }, |
| 130 | }); |
| 131 | const { openBlankSession, handleNewTab, onResumeSession, switchFolder, handleNavigateTopic } = navigationCommands; |
| 132 | |
| 133 | // Command palette: ⌘K / Ctrl+K opens a fuzzy navigator over commands and |
| 134 | // recent sessions. Sessions are snapshotted on open so the list is stable |
| 135 | // while the palette is up; extension actions follow the same snapshot rule. |
| 136 | const { openPalette, paletteItems } = usePaletteCommands({ |
| 137 | managementActive, |
| 138 | activeTabId, |
| 139 | remoteSurfaceActive, |
| 140 | t, |
| 141 | notice, |
| 142 | showToast, |
| 143 | ports: { |
| 144 | handleNewTab: () => void handleNewTab(), |
| 145 | listSessions, |
| 146 | openTrash: () => void openTrash(), |
| 147 | onResumeSession: (session) => onResumeSession(session), |
| 148 | openRemoteWorkspaceFromStatus: (host) => openRemoteWorkspaceFromStatus(host), |
| 149 | connectAndOpenRemoteWorkspace: (host) => connectAndOpenRemoteWorkspace(host), |
| 150 | toggleTerminalPanel, |
| 151 | setTasksOpen: (open) => setTasksOpen(open), |
| 152 | handleTabClose: (id) => void handleTabClose(id), |
| 153 | toggleSidebar, |
| 154 | returnToWorkspace, |
| 155 | }, |
| 156 | }); |
| 157 | |
| 158 | // --- Topic shortcut navigation (Cmd/Ctrl+1-9) --- |
| 159 | const { showBadges: showTopicBadges, setVisibleTopics: handleVisibleTopicsChange } = useTopicNavigationShortcuts({ |
| 160 | enabled: !sidebarCollapsed && !managementActive, |
| 161 | platform: desktopPlatform, |
| 162 | onNavigate: handleNavigateTopic, |
| 163 | }); |
| 164 | |
| 165 | // Delete / rename act on disk, then re-fetch so the panel reflects the change. |
| 166 | // Workspace: open the folder chooser and switch projects. The hook resets the |
| 167 | // transcript and refreshes meta on a pick. A cancel is a no-op. |
| 168 | const projectTopicCommands = useProjectTopicCommands({ |
| 169 | visible: { tabId: activeTabId ?? "", sessionKey: activeSessionIdentity }, |
| 170 | topic: activeTab?.remote ? { |
| 171 | id: activeTab.id, title: activeTab.topicTitle || "", |
| 172 | target: { kind: "remote", ...activeTab.remote, sessionPath: activeTab.sessionPath || "", sessionId: activeTab.session?.sessionId || activeTab.sessionId }, |
| 173 | } : activeTab?.topicId ? { |
| 174 | id: activeTab.topicId, title: activeTab.topicTitle || "", target: { kind: "local", topicId: activeTab.topicId, |
| 175 | selector: { ref: activeTab.session ?? undefined, sessionPath: activeTab.sessionPath } }, |
| 176 | } : undefined, |
| 177 | ports: { ...desktopProjectAdapter, markChanged: setProjectRevision, refreshTabs: refreshTabMetas, syncActive: syncActiveTab }, |
| 178 | navigation: { openBlank: openBlankSession, enqueue: enqueueNavigation, switchFolder }, |
| 179 | reportError: error => showToast(error instanceof Error ? error.message : String(error), "error"), |
| 180 | }); |
| 181 | |
| 182 | const sidebarExpandBlocked = false; |
| 183 | const sidebarToggleTitle = sidebarCollapsed |
| 184 | ? t("sidebar.expand") |
| 185 | : t("sidebar.collapse"); |
| 186 | const browserPreviewChrome = typeof window !== "undefined" && desktopHost().kind === "none"; |
| 187 | const browserMockScenario = browserPreviewChrome ? browserMockScenarioParam() : ""; |
| 188 | const guidanceQueueMockItems = isGuidanceMockScenario(browserMockScenario) ? GUIDANCE_QUEUE_MOCK_ITEMS : undefined; |
| 189 | // Command palette shortcut label (⌘K / Ctrl+K), platform-aware. |
| 190 | const commandPaletteShortcut = formatShortcutCombo( |
| 191 | resolvedShortcutCombo("commandPalette.open", desktopPlatform), |
| 192 | desktopPlatform, |
| 193 | ); |
| 194 | const chromeCommands = useAppChromeCommands({ |
| 195 | platform: desktopPlatform, |
| 196 | windowsFrameless: windowsFramelessChrome, |
| 197 | closeTransientOverlays, |
| 198 | clearImDetail: () => setSidebarImDetailConnectionId(""), |
| 199 | setSettingsFocus, |
| 200 | setSettingsTarget, |
| 201 | setSidebarSearchOpen, |
| 202 | setSidebarSearchFocusSignal, |
| 203 | refreshMeta, |
| 204 | refreshProviderSetupState, |
| 205 | reloadDesktopPreferences: (settings) => reloadDesktopPreferences(settings), |
| 206 | }); |
| 207 | const onboardingCommands = useOnboardingCommands(() => setProviderSetupNeeded(false)); |
| 208 | const worktreeMergeCommands = useWorktreeMergeCommands({ |
| 209 | noteNavigationIntent, |
| 210 | registeredNavigationIntent, isNavigationIntentCurrent, ensureBlankSurface, |
| 211 | seedSource: seedActiveTabMeta, listTabs: desktopBridge.listTabs, |
| 212 | closeWorktree: desktopBridge.closeMergedWorktreeTab, finalize: desktopBridge.finalizeWorktreeMerge, |
| 213 | showToast, t, showCleanup: (cleanup, translate) => showWorktreeCleanupNotice(cleanup, translate, showToast), |
| 214 | }); |
| 215 | return { |
| 216 | historyCommands, |
| 217 | navigationCommands, |
| 218 | paletteCommands: { openPalette, paletteItems }, |
| 219 | topicShortcuts: { showTopicBadges, handleVisibleTopicsChange }, |
| 220 | projectTopicCommands, |
| 221 | chromeCommands, |
| 222 | onboardingCommands, |
| 223 | worktreeMergeCommands, |
| 224 | sidebarExpandBlocked, |
| 225 | sidebarToggleTitle, |
| 226 | browserPreviewChrome, |
| 227 | commandPaletteShortcut, |
| 228 | guidanceQueueMockItems, |
| 229 | }; |
| 230 | } |
| 231 |