返回 DeepSeek-Reasonix
useTabBarCommands.ts
根目录 / desktop / frontend / src / app-runtime / useTabBarCommands.ts
1 import { useState, type Dispatch, type SetStateAction } from "react";
2 import { app } from "../lib/bridge";
3 import { useCommittedCommand } from "../lib/useCommittedCommand";
4 import { guardBackendNavigationResult } from "../lib/navigationSurfaceTransition";
5 import { useOverlayStore } from "../store/overlays";
6 import type { ActiveWorkView, TabMeta } from "../lib/types";
7 import type { ComposerProfile } from "../lib/composerProfile";
8 import type { Translator } from "../lib/i18n";
9
10 export type TabClosePolicy = "keep_running" | "stop_and_close";
11
12 export type TabBarCommandsInput = {
13 activeTabId: string | undefined;
14 tabMetas: readonly TabMeta[];
15 deliveryWorktreeRoot: string | undefined;
16 t: Translator;
17 showToast(message: string, level: "error", options?: { durationMs?: number }): void;
18 setTabMetas: Dispatch<SetStateAction<TabMeta[]>>;
19 // setTabOrderIds, ports.reorderTabs and ports.switchRemoteTab lost their only
20 // reader with the app tab strip; they stay declared until the caller drops them.
21 setTabOrderIds: Dispatch<SetStateAction<string[]>>;
22 setComposerProfilesByTab: Dispatch<SetStateAction<Record<string, ComposerProfile>>>;
23 setTabRevealSignal: Dispatch<SetStateAction<number>>;
24 clearWorkspaceConflict(): void;
25 ports: {
26 closeTab(id: string, policy: TabClosePolicy): Promise<boolean>;
27 reorderTabs(ids: string[]): Promise<void>;
28 switchTab(id: string, tab?: TabMeta, seq?: number): Promise<unknown>;
29 switchRemoteTab(tab: TabMeta, seq?: number): Promise<unknown>;
30 refreshTabMetas(apply?: () => boolean, options?: { afterMutation?: boolean }): Promise<TabMeta[]>;
31 refreshBackgroundRuntimes(): Promise<void>;
32 cancelActive(): void;
33 noteNavigationIntent(): number;
34 beginNavigationSurface(seq: number): void;
35 settleNavigationSurface(seq: number): void;
36 isNavigationIntentCurrent(seq: number): boolean;
37 reassertVisibleTabAfterStaleNavigation(kind: string, staleTabId: string): Promise<void>;
38 enterChatView(): void;
39 createIsolatedWorktree(root: string, seq: number): Promise<unknown>;
40 };
41 };
42
43 /**
44 * Owns the tab close command and prompt, the background-runtime reveals and the
45 * delivery-worktree continuation. Tab close prompts and reveal navigation share
46 * one navigation-intent/surface lifecycle; only the visible tab list, reveal
47 * signal and close prompt stay on the caller's stores.
48 */
49 export function useTabBarCommands(input: TabBarCommandsInput) {
50 const { activeTabId, t, showToast, ports } = input;
51 const [pendingClose, setPendingClose] = useState<{ tabId: string; work: ActiveWorkView; stopping: boolean } | null>(null);
52 const setTransientOverlayDismissSignal = useOverlayStore((state) => state.setTransientOverlayDismissSignal);
53
54 const closeTransientOverlays = useCommittedCommand(() => {
55 setTransientOverlayDismissSignal((signal) => signal + 1);
56 });
57
58 const enterChatViewForTabNavigation = useCommittedCommand(() => {
59 ports.enterChatView();
60 });
61
62 const revealBackgroundRuntime = useCommittedCommand(async (tabId: string): Promise<void> => {
63 enterChatViewForTabNavigation();
64 const navigationIntentSeq = ports.noteNavigationIntent();
65 ports.beginNavigationSurface(navigationIntentSeq);
66 try {
67 const meta = await app.RevealBackgroundRuntime(tabId);
68 if (!await guardBackendNavigationResult({
69 intent: navigationIntentSeq,
70 targetTabId: meta.id,
71 kind: "tab.reveal-background",
72 isIntentCurrent: ports.isNavigationIntentCurrent,
73 reassert: ports.reassertVisibleTabAfterStaleNavigation,
74 })) return;
75 await ports.switchTab(meta.id, meta, navigationIntentSeq);
76 if (!ports.isNavigationIntentCurrent(navigationIntentSeq)) return;
77 await ports.refreshTabMetas(
78 () => ports.isNavigationIntentCurrent(navigationIntentSeq),
79 { afterMutation: true },
80 );
81 } catch (err) {
82 if (ports.isNavigationIntentCurrent(navigationIntentSeq)) showToast(err instanceof Error ? err.message : String(err), "error");
83 } finally {
84 ports.settleNavigationSurface(navigationIntentSeq);
85 }
86 });
87
88 const finishTabClose = useCommittedCommand(async (
89 id: string,
90 policy: TabClosePolicy,
91 ): Promise<boolean> => {
92 closeTransientOverlays();
93 const closed = await ports.closeTab(id, policy);
94 if (!closed) {
95 showToast(t("runtime.closeFailed"), "error");
96 return false;
97 }
98 input.setComposerProfilesByTab((current) => {
99 if (!(id in current)) return current;
100 const next = { ...current };
101 delete next[id];
102 return next;
103 });
104 input.setTabMetas((current) => {
105 const closingIndex = current.findIndex((tab) => tab.id === id);
106 if (closingIndex < 0) return current;
107 const closingTab = current[closingIndex];
108 const remaining = current.filter((tab) => tab.id !== id);
109 if (remaining.length === 0) return [];
110 if (!closingTab.active && closingTab.id !== activeTabId) return remaining;
111 const nextIndex = Math.min(closingIndex, remaining.length - 1);
112 const nextActiveId = remaining[nextIndex]?.id;
113 return remaining.map((tab) => ({ ...tab, active: tab.id === nextActiveId }));
114 });
115 await ports.refreshTabMetas(undefined, { afterMutation: true });
116 await ports.refreshBackgroundRuntimes();
117 input.setTabRevealSignal((signal) => signal + 1);
118 return true;
119 });
120
121 const handleTabClose = useCommittedCommand(async (id: string) => {
122 try {
123 const work = await app.ActiveWorkForTab(id);
124 if (work.running || work.pendingPrompt || work.jobs.length > 0) {
125 setPendingClose({ tabId: id, work, stopping: false });
126 return;
127 }
128 } catch {
129 // CloseTabWithPolicy re-checks the controller state atomically.
130 }
131 await finishTabClose(id, "stop_and_close");
132 });
133
134 const resolvePendingClose = useCommittedCommand(async (policy: TabClosePolicy) => {
135 const request = pendingClose;
136 if (!request || request.stopping) return;
137 if (policy === "stop_and_close") setPendingClose({ ...request, stopping: true });
138 const closed = await finishTabClose(request.tabId, policy);
139 if (closed) setPendingClose(null);
140 else setPendingClose((current) => current?.tabId === request.tabId ? { ...current, stopping: false } : current);
141 });
142
143 const revealWorkspaceWriter = useCommittedCommand(async () => {
144 if (!activeTabId) return;
145 enterChatViewForTabNavigation();
146 const navigationIntentSeq = ports.noteNavigationIntent();
147 ports.beginNavigationSurface(navigationIntentSeq);
148 try {
149 const meta = await app.RevealWorkspaceWriterForTab(activeTabId);
150 if (!await guardBackendNavigationResult({
151 intent: navigationIntentSeq,
152 targetTabId: meta.id,
153 kind: "tab.reveal-workspace-writer",
154 isIntentCurrent: ports.isNavigationIntentCurrent,
155 reassert: ports.reassertVisibleTabAfterStaleNavigation,
156 })) return;
157 input.clearWorkspaceConflict();
158 await ports.switchTab(meta.id, meta, navigationIntentSeq);
159 if (!ports.isNavigationIntentCurrent(navigationIntentSeq)) return;
160 await ports.refreshTabMetas(
161 () => ports.isNavigationIntentCurrent(navigationIntentSeq),
162 { afterMutation: true },
163 );
164 } catch (err) {
165 if (ports.isNavigationIntentCurrent(navigationIntentSeq)) showToast(err instanceof Error ? err.message : String(err), "error");
166 } finally {
167 ports.settleNavigationSurface(navigationIntentSeq);
168 }
169 });
170
171 const continueInDeliveryWorktree = useCommittedCommand(async () => {
172 const root = input.deliveryWorktreeRoot;
173 if (!root) return;
174 ports.cancelActive();
175 input.clearWorkspaceConflict();
176 const navigationIntentSeq = ports.noteNavigationIntent();
177 ports.beginNavigationSurface(navigationIntentSeq);
178 try {
179 await ports.createIsolatedWorktree(root, navigationIntentSeq);
180 await ports.refreshTabMetas(undefined, { afterMutation: true });
181 } catch (err) {
182 if (ports.isNavigationIntentCurrent(navigationIntentSeq)) showToast(err instanceof Error ? err.message : String(err), "error");
183 } finally {
184 ports.settleNavigationSurface(navigationIntentSeq);
185 }
186 });
187
188 return {
189 pendingClose,
190 setPendingClose,
191 revealBackgroundRuntime,
192 finishTabClose,
193 handleTabClose,
194 resolvePendingClose,
195 revealWorkspaceWriter,
196 continueInDeliveryWorktree,
197 };
198 }
199
199 lines TYPESCRIPT