返回 DeepSeek-Reasonix
useAppSessionComposition.ts
根目录 / desktop / frontend / src / app-runtime / useAppSessionComposition.ts
1 import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
2 import { useCommittedCommand } from "../lib/useCommittedCommand";
3 import { projectSessionAvailability } from "../lib/sessionAvailability";
4 import type { RemoteSessionApi } from "../lib/useRemoteSession";
5 import { activeLeaseBlockedTab } from "../lib/tabMetaRefresh";
6 import { topicTitle } from "../lib/sessionTitles";
7 import { composerDraftKeyForTab } from "../lib/composerDraftKey";
8 import { useWindowStatePersistence, useViewportHeightVar } from "../lib/windowState";
9 import { useManagementWorkspace } from "../lib/useManagementWorkspace";
10 import { reportPendingRevisionFailure, usePendingPlanRevisions } from "../lib/usePendingPlanRevisions";
11 import { useComposerModeActions } from "../lib/useComposerModeActions";
12 import { useRemoteComposerRuntimeActions, useRemoteComposerSend } from "../lib/useRemoteComposerIntegration";
13 import type { RemoteNavigationCommand } from "../lib/remoteNavigationCommands";
14 import type { CollaborationMode, TabMeta } from "../lib/types";
15 import type { ComposerProfile, UserPlanModeIntents } from "../lib/composerProfile";
16 import type { State } from "../lib/useController";
17 import type { Translator } from "../lib/i18n";
18 import type { useAppRuntimeAdapter } from "./useAppRuntimeAdapter";
19 import type { useNavigationSurface } from "../lib/useNavigationSurface";
20 import { desktopBridge } from "./desktopBridgeAdapter";
21 import { useSessionOperations } from "./useSessionOperations";
22 import { useComposerInsertCommands } from "./useComposerInsertCommands";
23 import { useSessionClearCommands } from "./useSessionClearCommands";
24 import { useRuntimeStatus } from "./useRuntimeStatus";
25 import { useAppDiagnostics, useRecoverableErrorToasts, useSidebarConnectionValidity } from "./useAppEffectHosts";
26 import { useActiveTabUiReset, useDecisionSurfaceFocus } from "./useLocalUiLifecycles";
27 import { useActiveTabMirrorCommit } from "./activeTabMirror";
28 import { useInvocationMetadata } from "./useInvocationMetadata";
29 import { useFooterHeightLifecycle } from "./useFooterHeightLifecycle";
30 import { useNativeSettingsEvent } from "./useNativeSettingsEvent";
31 import { useWindowsMaximisedSync } from "./useNativeWindowController";
32 import { useShellGeometry } from "./useShellGeometry";
33 import { useTopicSummary } from "./useTopicSummary";
34 import { useComposerProfileProjection } from "./useComposerProfileProjection";
35 import { useTabBarCommands } from "./useTabBarCommands";
36 import { useExtensionSurface } from "./useExtensionSurface";
37 import { useTabProjectionLifecycle } from "./useTabProjectionLifecycle";
38 import { useSessionUndo } from "./useSessionUndo";
39 import { useSessionSubmission } from "../lib/useSessionSubmission";
40 import { useControllerProfileCommands } from "../lib/useControllerProfileCommands";
41 import { noteNavigationComposerEnabled } from "../lib/sessionDiagnostics";
42 import { useSessionPromptCommands } from "./useSessionPromptCommands";
43 import { useSessionControlCommands } from "./useSessionControlCommands";
44 import { useTodoPanelCommands } from "./useTodoPanelCommands";
45 import { useSessionExportCommands } from "./useSessionExportCommands";
46 import { useComposerRouter } from "./useComposerRouter";
47 import { useComposerGoalCommands } from "./useComposerGoalCommands";
48 import { useRuntimeEventHandlers } from "./useRuntimeEventHandlers";
49 import { probeProviderSetupState } from "./StartupGateLifecycle";
50 import { useSessionBannerCommands } from "./useSessionBannerCommands";
51 import { useWorkspacePanelCommands } from "./useWorkspacePanelCommands";
52 import { useTurnVerificationCommands } from "./useTurnVerificationCommands";
53 import { useTerminalPanelCommands } from "./useTerminalPanelCommands";
54 import { useRemoteWorkspaceCommands } from "./useRemoteWorkspaceCommands";
55 import { useAutomationNavigation } from "./useAutomationNavigation";
56 import { useDesktopNavigation } from "./useDesktopNavigation";
57 import { useTranscriptSurfaceProjection } from "./useTranscriptSurfaceProjection";
58 import { useDeliveryContinueCommands } from "./useDeliveryContinueCommands";
59 import { projectConversation, projectConversationLayout, projectWorkspaceScopeKey, projectWorkspaceTreeMemoryKey } from "./conversationProjection";
60 import { projectControllerProfiles, projectVisibleTabs } from "./controllerProfileOwner";
61 import { projectDecisionSurface, type AppDecisionSurfaceKind } from "./decisionSurfaceProjection";
62 import { createSubmissionPorts, projectSubmissionResources } from "./desktopSubmissionAdapter";
63 import type { useAppShellStores } from "./useAppShellStores";
64
65 function setRemoteComposerProfileForSessionAction(
66 tabId: string,
67 mode: CollaborationMode,
68 approvalMode: import("../lib/types").ToolApprovalMode,
69 goal: string,
70 ) {
71 return desktopBridge.setRemoteTabComposerProfile(tabId, mode, approvalMode, goal);
72 }
73
74 const WORKSPACE_RESIZER_WIDTH = 8;
75
76 type Runtime = ReturnType<typeof useAppRuntimeAdapter>;
77 type Shell = ReturnType<typeof useAppShellStores>;
78 type Surface = ReturnType<typeof useNavigationSurface>;
79 type LiveStore = Runtime["snapshot"]["liveStore"];
80
81 export type AppSessionCompositionInput = {
82 runtime: Runtime;
83 t: Translator;
84 showToast: (message: string, level?: "info" | "warn" | "error", options?: { durationMs?: number }) => void;
85 shell: Shell;
86 core: {
87 state: State;
88 liveStore: LiveStore;
89 activeTabId: string | undefined;
90 notice: Runtime["snapshot"]["notice"];
91 activeTab: TabMeta | undefined;
92 remoteSurfaceActive: boolean;
93 remoteSession: RemoteSessionApi;
94 remoteComposerReady: boolean;
95 remoteSend: (text: string, submitText?: string, choice?: import("../lib/modelApplication").ModelApplicationChoice) => Promise<void>;
96 remoteCancel: (queuedItemIDs?: string[]) => Promise<import("../lib/inboxCancel").CancelOutcome>;
97 activeSessionIdentity: string;
98 sessionSurfaceFence: ReturnType<typeof import("./sessionTarget").createSessionSurfaceFence>;
99 sessionOperations: ReturnType<typeof useSessionOperations>;
100 };
101 surface: Surface;
102 stores: {
103 composerProfilesByTab: Record<string, ComposerProfile>;
104 setComposerProfilesByTab: React.Dispatch<React.SetStateAction<Record<string, ComposerProfile>>>;
105 tabMetas: TabMeta[];
106 setTabMetas: React.Dispatch<React.SetStateAction<TabMeta[]>>;
107 tabOrderIds: string[];
108 setTabOrderIds: React.Dispatch<React.SetStateAction<string[]>>;
109 userPlanModeByTabRef: { current: UserPlanModeIntents };
110 };
111 local: {
112 setHistView: React.Dispatch<React.SetStateAction<import("./historyViewProjection").HistoryViewState | null>>;
113 setTabRevealSignal: React.Dispatch<React.SetStateAction<number>>;
114 sidebarImDetailConnectionId: string;
115 setSidebarImDetailConnectionId: React.Dispatch<React.SetStateAction<string>>;
116 workspaceScopeActiveTabRef: { current: string | undefined };
117 workspaceControllerEpoch: number;
118 setWorkspaceControllerEpoch: React.Dispatch<React.SetStateAction<number>>;
119 dockRefreshKey: number;
120 setDockRefreshKey: React.Dispatch<React.SetStateAction<number>>;
121 fileRefRefreshKey: number;
122 setFileRefRefreshKey: React.Dispatch<React.SetStateAction<number>>;
123 projectRevision: number;
124 setProjectRevision: React.Dispatch<React.SetStateAction<number>>;
125 };
126 goal: {
127 runGoalAction: ReturnType<typeof import("../lib/goalAction").useGoalActionHandler>["runGoalAction"];
128 handleGoalActionError: ReturnType<typeof import("../lib/goalAction").useGoalActionHandler>["handleGoalActionError"];
129 };
130 };
131
132 /**
133 * Session/composer composition: runs every session-domain owner hook in the
134 * App body's original order and returns the bags the navigation composition
135 * and the shell view consume. Pure relocation — hook order within the
136 * segment is unchanged.
137 */
138 export function useAppSessionComposition(input: AppSessionCompositionInput) {
139 const { t, showToast, shell, runtime } = input;
140 useRecoverableErrorToasts(showToast);
141 const {
142 state, liveStore, activeTabId, notice, activeTab, remoteSurfaceActive, remoteSession, remoteComposerReady,
143 remoteSend, activeSessionIdentity, sessionSurfaceFence, sessionOperations,
144 } = input.core;
145 // remoteCancel is consumed by the shell view through core.
146 const {
147 transitioning: runtimeTransitioning, dataReady: navigationTargetDataReady,
148 preserved: preservedTranscriptSurface, commitRendered: commitRenderedTranscriptSurface,
149 begin: beginNavigationSurface, maskTarget: settleNavigationSurface, commitPaint: commitNavigationSurfacePaint,
150 } = input.surface;
151 const {
152 sendToTab, runShellForTab, steerForTab, cancel, cancelForTab,
153 setControllerModeForTab, setCollaborationMode: setControllerCollaborationMode,
154 setCollaborationModeForTab: setControllerCollaborationModeForTab,
155 setToolApprovalModeForTab,
156 setComposerProfileForTab: setControllerComposerProfileForTab, setGoalForTab: setControllerGoalForTab,
157 editGoalForTab: editControllerGoalForTab,
158 resumeGoalForTab: resumeControllerGoalForTab, pauseGoalForTab: pauseControllerGoalForTab,
159 clearGoalForTab: clearControllerGoalForTab,
160 setModelForTab, setEffortForTab,
161 } = runtime.composer;
162 const {
163 recoverDeliveryToTab, approveForTab, isPromptCurrentForTab, resolvePlanDecisionForTab, resolveRecoveryForTab,
164 answerQuestionForTab, answerMCPInteractionForTab, dismissExtensionForm, drainExtensionNotifications,
165 clearSession, newSession, loadOlderHistory, loadNewerHistory, navigateToTurn, rewindForTab, rewindForTabDetailed, undoRewindForTab, forkTurnForTab,
166 listSessions, openChannelSession, resumeSession,
167 } = runtime.sessionActions;
168 const {
169 switchTab, switchRemoteTab, closeTab, reorderTabs, createIsolatedWorktree,
170 noteNavigationIntent, registeredNavigationIntent, isNavigationIntentCurrent, reassertVisibleTabAfterStaleNavigation,
171 commitSingleSurfaceNavigation, activateTopic,
172 ensureBlankSurface, openCanonicalSession,
173 } = runtime.navigation;
174 const {
175 setTransientOverlayDismissSignal, managementActive,
176 windowsFramelessChrome, rightDockMode,
177 workspacePanelOpen, workspacePanelMaximized, liveTerminalHeight, setLiveWorkspacePanelRenderWidth,
178 setRightDockTreeWidth, terminalPanelOpen, setSettingsTarget, enterConversation,
179 } = shell;
180 const { sidebarImConnections, reloadConfigWarnings } = shell.preferences;
181 const {
182 composerProfilesByTab, setComposerProfilesByTab, tabMetas, setTabMetas, tabOrderIds, setTabOrderIds,
183 userPlanModeByTabRef,
184 } = input.stores;
185 const {
186 setHistView, setTabRevealSignal,
187 sidebarImDetailConnectionId, setSidebarImDetailConnectionId,
188 workspaceScopeActiveTabRef, workspaceControllerEpoch, setWorkspaceControllerEpoch,
189 setDockRefreshKey, projectRevision, setProjectRevision,
190 } = input.local;
191 const { runGoalAction, handleGoalActionError } = input.goal;
192 const insertCommands = useComposerInsertCommands({
193 activeTabId,
194 sessionKey: activeSessionIdentity,
195 browserSessionPath: activeTab?.sessionPath ?? state.meta?.sessionPath,
196 approval: state.approval,
197 operations: sessionOperations,
198 t,
199 showToast,
200 ports: { terminalOutput: (tabId, terminalSessionId) => desktopBridge.terminalOutputForTab(tabId, terminalSessionId) },
201 });
202 const {
203 setInsertTarget: setWorkspaceInsertTarget, replaceComposerInsert,
204 } = insertCommands;
205 useWindowsMaximisedSync(windowsFramelessChrome);
206 const clearCommands = useSessionClearCommands({
207 activeTabId,
208 activeSessionIdentity,
209 remote: remoteSurfaceActive,
210 t,
211 notice,
212 operations: sessionOperations,
213 refreshDock: () => setDockRefreshKey((value) => value + 1),
214 ports: {
215 clearSession,
216 clearRemoteSession: (tabId) => desktopBridge.clearRemoteTabSession(tabId),
217 retryRemoteHydration: () => remoteSession.retryHydration(),
218 },
219 });
220 const { clearContextPending, setClearContextPending } = clearCommands;
221 const appRef = useRef<HTMLDivElement>(null);
222 const layoutRef = useRef<HTMLDivElement>(null);
223 useManagementWorkspace(layoutRef, managementActive);
224
225 // Persist window geometry across launches.
226 useWindowStatePersistence();
227 useViewportHeightVar();
228
229 const { backgroundRuntimes, workspaceConflict, setWorkspaceConflict, refreshBackgroundRuntimes } = useRuntimeStatus({
230 tabId: activeTabId, sessionKey: activeSessionIdentity, running: state.running,
231 });
232
233 const closeTransientOverlays = useCommittedCommand(() => {
234 setTransientOverlayDismissSignal((signal) => signal + 1);
235 });
236
237 useSidebarConnectionValidity({ connections: sidebarImConnections, setConnectionId: setSidebarImDetailConnectionId });
238
239 useNativeSettingsEvent({ closeTransientOverlays, setSettingsTarget });
240
241 const [footerHeight, setFooterHeight] = useState(0);
242 const footerRef = useRef<HTMLElement>(null);
243 const commitFooterHeight = useCommittedCommand((height: number) => setFooterHeight(height));
244 useFooterHeightLifecycle(footerRef, commitFooterHeight);
245 useActiveTabMirrorCommit(activeTabId);
246 const { invocationMetadataByTab, handleInvocationMetadataChange } = useInvocationMetadata();
247 const shellGeometry = useShellGeometry({ appRef, layoutRef });
248 const {
249 chatReservedWidth,
250 workspacePanelRenderWidth, workspacePanelOverlay, workspacePanelRenderable,
251 workspacePanelGridOpen, sidebarRenderWidth, terminalRenderHeight,
252 } = shellGeometry;
253
254 // Remote tab became ready: refresh the tab list so the spectator banner
255 // (takenOver) renders. The agent:ready event only fires for local tabs;
256 // remote tabs publish readiness via remote-tab:<id>:state, which
257 const conversationView = projectConversation({ local: state, remote: remoteSurfaceActive ? remoteSession : undefined,
258 tab: activeTab, activeTabId, backgroundRuntimes, connectingLabel: t("status.connecting") });
259 const visibleRuntimeState = conversationView.runtime;
260 const sidebarImDetailConnection = useMemo(
261 () => sidebarImConnections.find((connection) => connection.id === sidebarImDetailConnectionId) ?? null,
262 [sidebarImConnections, sidebarImDetailConnectionId],
263 );
264 const chatSurfaceVisible = true;
265 const { dockVisible: surfaceWorkspacePanelRenderable, dockGridOpen: surfaceWorkspacePanelGridOpen,
266 dockOverlay: surfaceWorkspacePanelOverlay,
267 terminalOpen: terminalSurfaceOpen } = projectConversationLayout({
268 chatVisible: chatSurfaceVisible, localToolsEnabled: conversationView.localToolsEnabled, dockMode: rightDockMode,
269 dockRenderable: workspacePanelRenderable, dockGridOpen: workspacePanelGridOpen, dockOverlay: workspacePanelOverlay,
270 dockOpen: workspacePanelOpen, dockMaximized: workspacePanelMaximized, terminalOpen: terminalPanelOpen,
271 });
272 const statusBarVisible = chatSurfaceVisible && !sidebarImDetailConnection;
273 const composerSessionKey = useMemo(() => {
274 return composerDraftKeyForTab(activeTab, activeTabId);
275 }, [activeTab, activeTabId]);
276 const transcriptGeometrySessionKey = activeSessionIdentity;
277 const workspaceScopeKey = projectWorkspaceScopeKey({
278 activeTabId, sessionKey: activeSessionIdentity,
279 cwd: state.meta?.cwd, sessionGen: state.sessionGen, workspaceControllerEpoch,
280 });
281 const workspaceTreeMemoryKey = projectWorkspaceTreeMemoryKey({
282 scope: activeTab?.scope, workspaceRoot: activeTab?.workspaceRoot, cwd: state.meta?.cwd,
283 });
284 const { activeTopicTurns } = useTopicSummary({ activeTab, revision: projectRevision });
285 const visibleUserTurns = visibleRuntimeState.items.reduce((count, item) => (item.kind === "user" ? count + 1 : count), 0);
286 const currentTabTurns = Math.max(visibleRuntimeState.checkpoints.length, visibleUserTurns);
287 const sessionTurns = currentTabTurns > 0 ? currentTabTurns : remoteSurfaceActive ? 0 : activeTopicTurns ?? 0;
288 const startupSplashHold = !activeTabId && state.meta?.ready !== true && !state.meta?.startupErr;
289 const profileProjection = useComposerProfileProjection({
290 activeTabId,
291 activeTab,
292 meta: state.meta,
293 profilesByTab: composerProfilesByTab,
294 setProfilesByTab: setComposerProfilesByTab,
295 tabMetas,
296 remote: remoteSurfaceActive,
297 remoteSession,
298 planIntentsRef: userPlanModeByTabRef,
299 });
300 const {
301 composerProfile, goal, collaborationMode, toolApprovalMode,
302 patchComposerProfileForTab, patchActivatedGoalForTab,
303 } = profileProjection;
304 const controllerReady =
305 state.meta?.ready === true &&
306 (!state.meta.runtime || state.meta.runtime.phase === "ready") &&
307 !state.meta.startupErr &&
308 !state.backendActivationPending &&
309 !runtimeTransitioning;
310 useEffect(() => {
311 if (controllerReady && activeTabId && !remoteSurfaceActive) noteNavigationComposerEnabled(activeTabId);
312 }, [activeTabId, controllerReady, remoteSurfaceActive]);
313 useAppDiagnostics({ activeTabId, tabCount: tabMetas.length, ready: controllerReady, running: state.running,
314 hydrating: state.hydrating, runtimeTransitioning, contentRevision: state.historyLayoutRevision });
315
316 const tabBarCommands = useTabBarCommands({
317 activeTabId,
318 tabMetas,
319 deliveryWorktreeRoot: state.meta?.workspaceRoot || state.meta?.workspacePath || state.meta?.cwd,
320 t,
321 showToast,
322 setTabMetas,
323 setTabOrderIds,
324 setComposerProfilesByTab,
325 setTabRevealSignal,
326 clearWorkspaceConflict: () => setWorkspaceConflict(null),
327 ports: {
328 closeTab,
329 reorderTabs,
330 switchTab,
331 switchRemoteTab,
332 refreshTabMetas: (apply, options) => refreshTabMetas(apply, options),
333 refreshBackgroundRuntimes,
334 cancelActive: () => void handleCancelActive(),
335 noteNavigationIntent,
336 beginNavigationSurface,
337 settleNavigationSurface,
338 isNavigationIntentCurrent,
339 reassertVisibleTabAfterStaleNavigation,
340 enterChatView: enterConversation,
341 createIsolatedWorktree,
342 },
343 });
344 const { pendingClose, setPendingClose } = tabBarCommands;
345
346 const decisionSurface = useMemo((): AppDecisionSurfaceKind | null => projectDecisionSurface({
347 approval: state.approval, ask: state.ask, mcpInteraction: state.mcpInteraction, extensionForm: state.extensionForm,
348 workspaceConflict, pendingClose, clearContextPending, remote: remoteSurfaceActive,
349 }), [clearContextPending, pendingClose, remoteSurfaceActive, state.approval, state.ask, state.extensionForm, state.mcpInteraction, workspaceConflict]);
350 const visibleDecisionSurface = decisionSurface;
351 // Navigation used to hide the entire composer until the controller/runtime
352 // activation ticket and the transcript paint ticket both settled. That made
353 // a local session switch look like a frozen blank surface even though its
354 // history was already available (or could be shown from the bounded
355 // transcript cache). Keep the composer mounted during a local transition:
356 // the submission resources still use `controllerReady` as the write fence,
357 // so drafts remain editable while send/control actions stay disabled until
358 // the target runtime is ready. Decision surfaces remain exclusive.
359 const composerSurfaceHidden = (runtimeTransitioning && remoteSurfaceActive) || Boolean(decisionSurface);
360 useDecisionSurfaceFocus({ surface: decisionSurface, activeTabId, closeOverlays: closeTransientOverlays });
361
362 // Extension form surface (stage 8b2): submit delivers the structured values
363 // to the owning sidecar; cancel reports values{"cancelled": true} over the
364 // same channel. A failed cancel still dismisses — the sidecar that could not
365 // be reached is gone either way.
366 const extensionSurface = useExtensionSurface({
367 activeTabId,
368 hostId: state.meta?.session?.hostId,
369 sessionId: state.meta?.sessionId,
370 sessionGeneration: state.meta?.sessionGeneration,
371 form: state.extensionForm,
372 notifications: state.extensionNotifications,
373 dismissForm: dismissExtensionForm,
374 drainNotifications: drainExtensionNotifications,
375 showToast,
376 });
377 const extensionStatusList = useMemo(() => Object.values(state.extensionStatuses ?? {}), [state.extensionStatuses]);
378 const visibleTabId = activeTabId;
379 const visibleTabs = useMemo(() => projectVisibleTabs({
380 tabs: tabMetas, orderIds: tabOrderIds, profiles: composerProfilesByTab, visibleTabId, running: state.running,
381 }), [composerProfilesByTab, state.running, tabMetas, tabOrderIds, visibleTabId]);
382
383 useTabProjectionLifecycle({
384 tabs: tabMetas, activeTabId, activeMeta: activeTab, meta: state.meta,
385 planIntentsRef: userPlanModeByTabRef,
386 setOrder: setTabOrderIds, setProfiles: setComposerProfilesByTab,
387 });
388
389
390 const controllerProfiles = projectControllerProfiles(tabMetas, composerProfilesByTab, {
391 target: { tabId: activeTabId ?? "", sessionKey: activeSessionIdentity }, profile: composerProfile, remote: remoteSurfaceActive,
392 });
393 const controllerProfileCommands = useControllerProfileCommands({
394 target: { tabId: activeTabId ?? "", sessionKey: activeSessionIdentity }, profiles: controllerProfiles,
395 ready: controllerReady, remote: remoteSurfaceActive, runtimeEpoch: state.meta?.runtime?.epoch, operations: sessionOperations,
396 ports: { model: setModelForTab, profile: setControllerComposerProfileForTab },
397 remoteModel: remoteSession.setModel, report: handleGoalActionError,
398 });
399 const { switchModel, applyProfile: applyControllerProfile } = controllerProfileCommands;
400 const hydratePlaceholderActive = Boolean(
401 state.hydrating &&
402 state.items.length === 0 &&
403 state.hydratePlaceholderItems?.length,
404 );
405 const sessionUndoCommands = useSessionUndo({
406 activeTabId,
407 activeTabReadOnly: Boolean(activeTab?.readOnly),
408 items: state.items,
409 hydratePlaceholderActive,
410 controllerReady, running: state.running, messageActionOpen: state.messageAction != null,
411 approvalOpen: state.approval != null, askOpen: state.ask != null, clearContextPending,
412 ports: {
413 rewindForTab, rewindForTabDetailed, forkTurnForTab,
414 refreshTabMetas: () => void refreshTabMetas(undefined, { afterMutation: true }),
415 undoRewindForTab, sendToTab,
416 composeInsert: replaceComposerInsert,
417 refreshDock: () => setDockRefreshKey((value) => value + 1),
418 refreshProject: () => setProjectRevision((value) => value + 1),
419 },
420 });
421 const {
422 rewindState, rewindCommitting, rewindSignal, setRewindStateForTab,
423 handleSessionRevertCommitted, handleMessageAction, handleForkTurn, handleUndoRewind, handleEditPrompt,
424 } = sessionUndoCommands;
425 const clearSubmissionUndo = useCommittedCommand((tab: string) => setRewindStateForTab(tab, null));
426 const { commitThenSend, submit: submitComposerTurn, applyGoalForTab, applyGoal, sendRevision } = useSessionSubmission({
427 target: { tabId: activeTabId ?? "", sessionKey: activeSessionIdentity }, operations: sessionOperations,
428 resources: projectSubmissionResources(controllerProfiles, tabMetas, composerProfilesByTab,
429 { tabId: activeTabId ?? "", profile: composerProfile, ready: controllerReady },
430 { starting: t("composer.workspaceStarting"), readOnly: t("composer.readOnlyChannel") }),
431 missingSource: t("composer.workspaceStarting"),
432 ports: createSubmissionPorts({ send: sendToTab, setGoal: setControllerGoalForTab, clearGoal: clearControllerGoalForTab,
433 clearUndo: clearSubmissionUndo, patchGoal: patchActivatedGoalForTab, profile: applyControllerProfile }),
434 });
435 const patchPlanExitProfileForTab = useCommittedCommand((tabId: string, mode: CollaborationMode) => {
436 patchComposerProfileForTab(tabId, {
437 collaborationMode: mode,
438 goalDraftMode: false,
439 goal: "",
440 }, ["collaborationMode", "goal"]);
441 });
442 const drainRemoteApprovalsForTab = useCommittedCommand((tabId: string, ids: string[]) => {
443 if (activeTabId === tabId) remoteSession.drainApprovals(ids);
444 });
445 const modeActions = useComposerModeActions({
446 target: { tabId: activeTabId ?? "", sessionKey: activeSessionIdentity },
447 remote: remoteSurfaceActive, collaborationMode, toolApprovalMode, goal,
448 operations: sessionOperations,
449 planIntentsRef: userPlanModeByTabRef,
450 ports: {
451 setMode: setControllerModeForTab, setCollaboration: setControllerCollaborationModeForTab,
452 setApproval: setToolApprovalModeForTab, clearGoal: clearControllerGoalForTab,
453 setRemote: setRemoteComposerProfileForSessionAction, drainRemote: drainRemoteApprovalsForTab,
454 patch: patchComposerProfileForTab,
455 },
456 showError: (message) => showToast(message, "error"),
457 });
458 const { applyCollaborationMode, notePlanModeForTab } = modeActions;
459 const rememberPlanRevisionForTab = usePendingPlanRevisions({
460 visible: { tabId: activeTabId ?? "", sessionKey: activeSessionIdentity },
461 resources: controllerProfiles.map(resource => resource.target), running: state.running,
462 ready: controllerReady && !state.approval && !state.ask && !state.mcpInteraction,
463 operations: sessionOperations, send: sendRevision, report: reportPendingRevisionFailure,
464 });
465 const promptCommands = useSessionPromptCommands({
466 target: { tabId: activeTabId ?? "", sessionKey: activeSessionIdentity },
467 session: state.meta?.session,
468 sessionGeneration: state.meta?.sessionGeneration,
469 approval: state.approval,
470 question: state.ask,
471 mcpInteraction: state.mcpInteraction,
472 remote: Boolean(activeTab?.remote), goal, toolApprovalMode,
473 operations: sessionOperations,
474 ports: {
475 approveForTab, isPromptCurrentForTab, resolvePlanForTab: resolvePlanDecisionForTab,
476 resolveRecoveryForTab, answerQuestionForTab, answerMCPForTab: answerMCPInteractionForTab,
477 setCollaborationModeForTab: setControllerCollaborationModeForTab,
478 clearGoalForTab: clearControllerGoalForTab, setRemoteComposerProfile: setRemoteComposerProfileForSessionAction,
479 patchComposerProfile: patchPlanExitProfileForTab, notePlanMode: notePlanModeForTab,
480 drainRemoteApprovals: drainRemoteApprovalsForTab, rememberRevision: rememberPlanRevisionForTab,
481 },
482 reportError: error => showToast(error instanceof Error ? error.message : String(error), "error"),
483 });
484 const remoteComposerSend = useRemoteComposerSend(activeTab?.remote, activeTabId, collaborationMode, goal,
485 remoteSession, remoteSend, applyGoalForTab, useCommittedCommand(() => setClearContextPending(true)),
486 { target: { tabId: activeTabId ?? "", sessionKey: activeSessionIdentity }, operations: sessionOperations,
487 navigateRemote: useCommittedCommand<RemoteNavigationCommand>((remote, options) => openRemoteProject(remote, options)) });
488 const controlCommands = useSessionControlCommands({
489 activeTabId,
490 resources: controllerProfiles.map(resource => resource.target),
491 operations: sessionOperations,
492 showToast,
493 clearWorkspaceConflict: () => setWorkspaceConflict(null),
494 ports: {
495 cancel,
496 cancelForTab,
497 acceptDelivery: (tabId) => desktopBridge.acceptDeliveryToTab(tabId),
498 disconnectRemote: (hostId) => desktopBridge.disconnectRemoteHost(hostId),
499 cancelJobForTab: (tabId, jobId) => desktopBridge.cancelJobForTab(tabId, jobId),
500 refreshBackgroundRuntimes,
501 },
502 });
503 const { handleCancelActive } = controlCommands;
504 // Shift+Tab toggles only the collaboration axis. Permission presets are
505 // changed explicitly through the composer permission selector.
506 const cycleMode = useCommittedCommand(() => {
507 runGoalAction(() => applyCollaborationMode(collaborationMode === "plan" ? "normal" : "plan"));
508 });
509
510 const todoPanelCommands = useTodoPanelCommands({
511 items: visibleRuntimeState.items,
512 running: visibleRuntimeState.running,
513 pendingPrompt: visibleRuntimeState.pendingPrompt,
514 meta: state.meta,
515 activeTab,
516 activeTabId,
517 remote: remoteSurfaceActive,
518 remoteReady: remoteComposerReady,
519 controllerReady,
520 sessionKey: activeSessionIdentity,
521 operations: sessionOperations,
522 t,
523 ports: {
524 remoteSend: (text) => remoteSend(text),
525 sendToTab: (tabId, text) => sendToTab(tabId, text),
526 },
527 });
528 const { showTodos, scopedTodoBatch, todos, dismissTodos, handleTodoContinue } = todoPanelCommands;
529
530 const sessionTitle = topicTitle(activeTab);
531 const exportItems = remoteSurfaceActive ? remoteSession.transcript.items : state.items;
532 const exportLive = remoteSurfaceActive
533 ? remoteSession.transcript.live
534 : liveStore.getSnapshot(activeTabId) ?? state.live;
535 const sessionHasContent = exportItems.length > 0 || Boolean(exportLive?.text || exportLive?.reasoning);
536
537 const sessionExportCommands = useSessionExportCommands({
538 selector: activeTab?.session?.sessionId ? { ref: activeTab.session } : activeTab?.sessionPath ? { sessionPath: activeTab.sessionPath } : { topicId: activeTab?.topicId },
539 tabId: activeTabId,
540 remote: remoteSurfaceActive,
541 sessionTitle,
542 items: exportItems,
543 live: exportLive,
544 hasContent: sessionHasContent,
545 t,
546 showToast,
547 });
548
549 useActiveTabUiReset({ activeTabId, setClearPending: setClearContextPending, setInsertTarget: setWorkspaceInsertTarget });
550
551 const routerCommands = useComposerRouter({
552 activeTabId,
553 goalDraftActive: collaborationMode === "goal" && !goal.trim(),
554 t,
555 notice,
556 showToast,
557 ports: {
558 runShellForTab,
559 switchModel: (name, tabId) => switchModel(name, tabId),
560 newSession: () => newSession(),
561 setSettingsTarget: (tab) => setSettingsTarget(tab),
562 setClearContextPending,
563 clearWorkspaceConflict: () => setWorkspaceConflict(null),
564 setWorkspaceConflict: (value) => setWorkspaceConflict(value),
565 setPendingClose: (value) => setPendingClose(value),
566 submitComposerTurn: (tab, display, submit, structured, submissionId) => submitComposerTurn(tab, display, submit, structured, submissionId),
567 steerForTab,
568 isRemoteTab: (tabId) => tabMetas.some((tab) => tab.id === tabId && tab.remote),
569 },
570 });
571
572 const editGoal = async (objective: string, maxGoalRounds: number | null) => {
573 if (!activeTabId) return;
574 if (remoteSurfaceActive) await remoteSession.editGoal(objective, maxGoalRounds);
575 else await editControllerGoalForTab(activeTabId, objective, maxGoalRounds);
576 };
577 const goalCommands = useComposerGoalCommands({ applyCollaborationMode, applyGoal, editGoal });
578 const remoteGoalActions = useRemoteComposerRuntimeActions({
579 target: { tabId: activeTabId ?? "", sessionKey: activeSessionIdentity }, operations: sessionOperations,
580 remote: remoteSurfaceActive, session: remoteSession, runGoalAction,
581 pauseLocal: pauseControllerGoalForTab, resumeLocal: resumeControllerGoalForTab,
582 setLocalEffort: setEffortForTab, showError: (message) => showToast(message, "error"),
583 });
584
585 const {
586 refreshTabMetas, seedActiveTabMeta,
587 handleRuntimeEvent, handleRuntimeReady, handleRuntimeRebuilt,
588 handleRemoteStatus, handleRemoteForwards, handleRemoteServer,
589 handleInitialRemoteHosts, handleInitialRemoteStatuses,
590 } = useRuntimeEventHandlers({
591 activeTabId,
592 workspaceScopeKey,
593 workspaceScopeActiveTabRef,
594 userPlanModeByTabRef,
595 setTabMetas,
596 setTabOrderIds,
597 setComposerProfilesByTab,
598 setDockRefreshKey,
599 setProjectRevision,
600 setWorkspaceControllerEpoch,
601 setControllerCollaborationMode,
602 });
603
604 const refreshProviderSetupState = useCommittedCommand(() => probeProviderSetupState());
605
606 const leaseBlockedTab = activeLeaseBlockedTab(tabMetas, activeTab?.id ?? activeTabId);
607 const bannerCommands = useSessionBannerCommands({
608 remote: Boolean(activeTab?.remote),
609 reloadConfigWarnings,
610 });
611
612 const workspacePanelCommands = useWorkspacePanelCommands({
613 sessionId: activeTabId ?? "",
614 workspaceRoot: activeTab?.workspaceRoot ?? state.meta?.cwd ?? "",
615 visible: surfaceWorkspacePanelRenderable,
616 closeOverlays: closeTransientOverlays, clearLiveWidth: setLiveWorkspacePanelRenderWidth,
617 setTreeWidth: setRightDockTreeWidth,
618 gridOpen: surfaceWorkspacePanelGridOpen,
619 t,
620 });
621 const { openRightDockMode } = workspacePanelCommands;
622
623 const turnVerificationCommands = useTurnVerificationCommands({
624 activeTabId,
625 turnStartAt: state.turnStartAt,
626 completionSummary: state.completionSummary,
627 sessionPath: state.meta?.sessionPath,
628 openChangedDock: () => openRightDockMode("changed"),
629 });
630
631 const terminalPanelCommands = useTerminalPanelCommands({
632 tabId: activeTabId, enabled: conversationView.localToolsEnabled, shortcutsEnabled: !managementActive,
633 });
634
635 const remoteWorkspaceCommands = useRemoteWorkspaceCommands({ t, showToast });
636
637 const layoutStyle = useMemo(
638 () =>
639 ({
640 "--sidebar-expanded-width": `${sidebarRenderWidth}px`,
641 "--chat-min-width": `${chatReservedWidth}px`,
642 "--workspace-width": `${workspacePanelRenderWidth}px`,
643 "--workspace-resizer-width": `${WORKSPACE_RESIZER_WIDTH}px`,
644 "--terminal-height": `${terminalSurfaceOpen ? liveTerminalHeight ?? terminalRenderHeight : 0}px`,
645 }) as CSSProperties,
646 [chatReservedWidth, liveTerminalHeight, sidebarRenderWidth, terminalRenderHeight, terminalSurfaceOpen, workspacePanelRenderWidth],
647 );
648
649 // Coalesce tab-bar switches through the same last-click-wins scheduler that
650 // openTopic/blank/resume navigation uses, so rapidly clicking between two
651 // running sessions can't run two switchTab() calls concurrently. Concurrent
652 // switches race on the backend SetActiveTab/confirmBackendActiveTab ordering,
653 const availability = projectSessionAvailability({ local: state, remote: remoteSurfaceActive ? remoteSession : undefined, empty: !activeTabId && !runtimeTransitioning });
654 const presentationTransitioning = runtimeTransitioning && remoteSurfaceActive;
655 const {
656 transcriptHydrating, emptyHero,
657 visibleTranscriptItems, visibleTranscriptTabId, visibleTranscriptGeometryKey,
658 handleLoadOlderHistory, handleLoadNewerHistory, handleNavigateToTurn, handleSurfacePaintReady, latestGuidanceConsumed, handleTranscriptPrompt,
659 } = useTranscriptSurfaceProjection({
660 hydrating: state.hydrating,
661 hydrateHistoryLoaded: state.hydrateHistoryLoaded,
662 hydratePlaceholderItems: state.hydratePlaceholderItems,
663 hydratePlaceholderActive,
664 items: state.items,
665 guidanceConsumed: remoteSurfaceActive ? remoteSession.transcript.guidanceConsumed : state.guidanceConsumed,
666 remote: remoteSurfaceActive,
667 remoteItems: remoteSession.transcript.items,
668 activeTabId,
669 geometrySessionKey: transcriptGeometrySessionKey,
670 transitioning: presentationTransitioning,
671 navigationDataReady: navigationTargetDataReady,
672 preserved: preservedTranscriptSurface,
673 controllerReady,
674 availability,
675 sessionActivity: Boolean(conversationView.runtime.running || conversationView.runtime.pendingPrompt
676 || conversationView.runtime.approval || conversationView.runtime.ask || conversationView.runtime.extensionForm
677 || conversationView.runtime.mcpInteraction || (remoteSurfaceActive && (remoteSession.promptError || remoteSession.error))),
678 imDetailActive: Boolean(sidebarImDetailConnection),
679 sessionHasContent,
680 commitRendered: commitRenderedTranscriptSurface,
681 commitPaint: commitNavigationSurfacePaint,
682 commitSingleSurface: commitSingleSurfaceNavigation,
683 ports: {
684 loadOlderHistory: (tabId, targetTurn, trigger) => loadOlderHistory(tabId, targetTurn, trigger),
685 loadNewerHistory: (tabId, latest, current) => loadNewerHistory(tabId, latest, current),
686 navigateToTurn,
687 commitThenSend: (tabId, text, submitText) => commitThenSend(tabId, text, submitText),
688 },
689 });
690
691 const { handleDeliveryContinue } = useDeliveryContinueCommands({
692 surfaceFence: sessionSurfaceFence,
693 ready: controllerReady,
694 goal: state.meta?.goal,
695 t,
696 ports: {
697 resumeGoal: resumeControllerGoalForTab,
698 recoverDelivery: recoverDeliveryToTab,
699 },
700 });
701
702 const { openAutomationTopic, topicAccepted } = useAutomationNavigation({ noteIntent: noteNavigationIntent,
703 enqueue: useCommittedCommand((intent, seq) => enqueueNavigationWithIntent(intent, seq)) }); const {
704 enqueueNavigation, enqueueNavigationWithIntent, openRemoteProject,
705 } = useDesktopNavigation({
706 visible: { tabId: activeTabId ?? "", sessionKey: activeSessionIdentity },
707 ports: { isNavigationIntentCurrent, activateTopic,
708 ensureBlankSurface, openCanonicalSession, createIsolatedWorktree, openChannelSession, resumeSession,
709 registeredNavigationIntent, switchRemoteTab, openRemoteProject: desktopBridge.openRemoteProjectTab,
710 listTabs: desktopBridge.listTabs, applyTabs: setTabMetas, seedTab: seedActiveTabMeta, listSessions, topicAccepted,
711 prepareSession: desktopBridge.prepareSession, getSessionPreparation: desktopBridge.getSessionPreparation },
712 setTabRevealSignal, setProjectRevision, setHistory: setHistView, t, showToast,
713 noteIntent: noteNavigationIntent, beginSurface: beginNavigationSurface, settleSurface: settleNavigationSurface,
714 showChat: enterConversation,
715 });
716 return {
717 insertCommands,
718 clearCommands,
719 tabBarCommands,
720 extensionSurface,
721 promptCommands,
722 controlCommands,
723 routerCommands,
724 goalCommands,
725 remoteGoalActions,
726 modeActions,
727 controllerProfileCommands,
728 profileProjection,
729 sessionExportCommands,
730 workspacePanelCommands,
731 turnVerificationCommands,
732 terminalPanelCommands,
733 remoteWorkspaceCommands,
734 bannerCommands,
735 runtimeEventCommands: {
736 refreshTabMetas, seedActiveTabMeta,
737 handleRuntimeEvent, handleRuntimeReady, handleRuntimeRebuilt,
738 handleRemoteStatus, handleRemoteForwards, handleRemoteServer,
739 handleInitialRemoteHosts, handleInitialRemoteStatuses,
740 },
741 sessionUndo: {
742 rewindState, rewindCommitting, rewindSignal, handleSessionRevertCommitted, handleMessageAction, handleForkTurn, handleUndoRewind, handleEditPrompt,
743 },
744 todoPanel: { showTodos, scopedTodoBatch, todos, dismissTodos, handleTodoContinue },
745 delivery: { handleDeliveryContinue },
746 transcript: {
747 transcriptHydrating, emptyHero, availability,
748 visibleTranscriptItems, visibleTranscriptTabId, visibleTranscriptGeometryKey,
749 handleLoadOlderHistory, handleLoadNewerHistory, handleNavigateToTurn, handleSurfacePaintReady, latestGuidanceConsumed, handleTranscriptPrompt,
750 },
751 automation: { openAutomationTopic },
752 desktopNavigation: { enqueueNavigation, enqueueNavigationWithIntent, openRemoteProject },
753 invocation: { invocationMetadataByTab, handleInvocationMetadataChange },
754 sessionHasContent,
755 conversationView,
756 visibleRuntimeState,
757 sidebarImDetailConnection,
758 surfaceWorkspacePanelRenderable,
759 surfaceWorkspacePanelGridOpen,
760 surfaceWorkspacePanelOverlay,
761 terminalSurfaceOpen,
762 statusBarVisible,
763 chatSurfaceVisible,
764 composerSessionKey,
765 workspaceScopeKey,
766 workspaceTreeMemoryKey,
767 sessionTurns,
768 startupSplashHold,
769 controllerReady,
770 decisionSurface,
771 visibleDecisionSurface,
772 composerSurfaceHidden,
773 extensionStatusList,
774 visibleTabs,
775 visibleTabId,
776 hydratePlaceholderActive,
777 leaseBlockedTab,
778 layoutStyle,
779 cycleMode,
780 remoteComposerSend,
781 closeTransientOverlays,
782 refreshProviderSetupState,
783 shellGeometry,
784 appRef,
785 layoutRef,
786 footerHeight,
787 footerRef,
788 backgroundRuntimes,
789 workspaceConflict,
790 };
791 }
792
792 lines TYPESCRIPT