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