返回 DeepSeek-Reasonix
useAppNavigationComposition.ts
根目录 / desktop / frontend / src / app-runtime / useAppNavigationComposition.ts
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
231 lines TYPESCRIPT