| 1 | import type { Todo } from "../lib/tools"; |
| 2 | import { modelSettingsAllowSubmission } from "../lib/authenticationTypes"; |
| 3 | import type { RewindUndoState } from "../lib/rewindTypes"; |
| 4 | import type { WorkspaceConflictView } from "../lib/types"; |
| 5 | import type { DecisionSurfaceKind as MockDecisionSurfaceKind } from "../lib/decisionSurfaceMock"; |
| 6 | import type { Translator } from "../lib/i18n"; |
| 7 | import type { projectConversation } from "../app-runtime/conversationProjection"; |
| 8 | import type { useSessionPromptCommands } from "../app-runtime/useSessionPromptCommands"; |
| 9 | import type { useExtensionSurface } from "../app-runtime/useExtensionSurface"; |
| 10 | import type { useSessionClearCommands } from "../app-runtime/useSessionClearCommands"; |
| 11 | import type { useTabBarCommands } from "../app-runtime/useTabBarCommands"; |
| 12 | import type { useComposerProfileProjection } from "../app-runtime/useComposerProfileProjection"; |
| 13 | import type { useComposerInsertCommands } from "../app-runtime/useComposerInsertCommands"; |
| 14 | import type { useComposerModeActions } from "../lib/useComposerModeActions"; |
| 15 | import type { useComposerGoalCommands } from "../app-runtime/useComposerGoalCommands"; |
| 16 | import type { useRemoteComposerRuntimeActions } from "../lib/useRemoteComposerIntegration"; |
| 17 | import type { useControllerProfileCommands } from "../lib/useControllerProfileCommands"; |
| 18 | import { draftSubmissionLocksEditing, type useSessionDraftSurface } from "../app-runtime/useSessionDraftSurface"; |
| 19 | import { draftSurfaceNeedsAttention } from "./draftPresentation"; |
| 20 | import type { |
| 21 | ApprovalProps, |
| 22 | AskProps, |
| 23 | ComposerProps, |
| 24 | DecisionFooterRegionProps, |
| 25 | DecisionFooterSurface, |
| 26 | ExtensionProps, |
| 27 | McpProps, |
| 28 | RuntimeDecisionProps, |
| 29 | TodoProps, |
| 30 | } from "./DecisionFooterRegion"; |
| 31 | |
| 32 | type SurfaceKind = MockDecisionSurfaceKind | "extension_form"; |
| 33 | type ComposerBase = ReturnType<typeof projectConversation>["composer"]; |
| 34 | type PromptCommands = ReturnType<typeof useSessionPromptCommands>; |
| 35 | type ExtensionSurfaceApi = ReturnType<typeof useExtensionSurface>; |
| 36 | type ClearCommands = Pick<ReturnType<typeof useSessionClearCommands>, "cancelClearContext" | "confirmClearContext">; |
| 37 | type TabBarApi = Pick<ReturnType<typeof useTabBarCommands>, "pendingClose" | "setPendingClose" | "resolvePendingClose" | "revealWorkspaceWriter" | "continueInDeliveryWorktree">; |
| 38 | |
| 39 | /** Pure prop builders for DecisionFooterRegion; every closure keeps the exact |
| 40 | * handler identity and branching the App body previously assembled inline. */ |
| 41 | |
| 42 | export function buildFooterTodo(input: { |
| 43 | show: boolean; |
| 44 | identity: string; |
| 45 | todos: Todo[]; |
| 46 | running: boolean; |
| 47 | pendingPrompt: boolean; |
| 48 | continueReady: boolean; |
| 49 | onContinue: TodoProps["onContinue"]; |
| 50 | onDismiss: TodoProps["onDismiss"]; |
| 51 | }): DecisionFooterRegionProps["todo"] { |
| 52 | if (!input.show) return undefined; |
| 53 | return { |
| 54 | identity: input.identity, |
| 55 | props: { |
| 56 | stateKey: input.identity, |
| 57 | todos: input.todos, |
| 58 | running: input.running, |
| 59 | pendingPrompt: input.pendingPrompt, |
| 60 | onContinue: input.continueReady ? input.onContinue : undefined, |
| 61 | onDismiss: input.onDismiss, |
| 62 | }, |
| 63 | }; |
| 64 | } |
| 65 | |
| 66 | export function buildFooterUndo(input: { |
| 67 | rewindState: RewindUndoState | null; |
| 68 | activeTabId: string | undefined; |
| 69 | onUndo: () => void; |
| 70 | }): DecisionFooterRegionProps["undo"] { |
| 71 | const { rewindState } = input; |
| 72 | if (!rewindState) return undefined; |
| 73 | return { |
| 74 | identity: `${input.activeTabId ?? ""}:${rewindState.transactionId ?? "rewind"}`, |
| 75 | props: { |
| 76 | meta: { |
| 77 | turns: rewindState.turnDiff, |
| 78 | filesRestored: rewindState.filesRestored ?? [], |
| 79 | filesRemoved: rewindState.filesRemoved ?? [], |
| 80 | onUndo: input.onUndo, |
| 81 | }, |
| 82 | }, |
| 83 | }; |
| 84 | } |
| 85 | |
| 86 | export type DecisionFooterSurfaceInput = { |
| 87 | view: { |
| 88 | surface: SurfaceKind | null; |
| 89 | activeTabId: string | undefined; |
| 90 | cwd: string | undefined; |
| 91 | workspaceScopeKey: string; |
| 92 | approval: ApprovalProps["approval"] | null | undefined; |
| 93 | ask: AskProps["ask"] | null | undefined; |
| 94 | mcpInteraction: McpProps["interaction"] | null | undefined; |
| 95 | extensionForm: ExtensionProps["surface"] | null | undefined; |
| 96 | workspaceConflict: WorkspaceConflictView | null; |
| 97 | toolApprovalMode: ApprovalProps["toolApprovalMode"]; |
| 98 | insertRequest: ApprovalProps["insertRequest"]; |
| 99 | }; |
| 100 | prompts: PromptCommands; |
| 101 | extension: ExtensionSurfaceApi; |
| 102 | tabs: TabBarApi; |
| 103 | clear: ClearCommands; |
| 104 | onStop: ApprovalProps["onStop"]; |
| 105 | cancelWorkspaceConflict: RuntimeDecisionProps["onCancel"]; |
| 106 | onOpenLink: McpProps["onOpenLink"]; |
| 107 | onRevisionActiveChange: ApprovalProps["onRevisionActiveChange"]; |
| 108 | t: Translator; |
| 109 | }; |
| 110 | |
| 111 | export function buildDecisionFooterSurface(input: DecisionFooterSurfaceInput): DecisionFooterSurface | undefined { |
| 112 | const { view, prompts, extension, tabs, clear, t } = input; |
| 113 | const { surface, activeTabId } = view; |
| 114 | if ((surface === "tool_approval" || surface === "plan_approval") && view.approval) { |
| 115 | return { |
| 116 | kind: "approval", |
| 117 | identity: prompts.approvalTarget?.instanceKey ?? `${activeTabId ?? ""}:${view.approval.id}`, |
| 118 | props: { |
| 119 | approval: view.approval, |
| 120 | cwd: view.cwd, |
| 121 | tabId: activeTabId, |
| 122 | workspaceScopeKey: view.workspaceScopeKey, |
| 123 | insertRequest: view.insertRequest, |
| 124 | onRevisionActiveChange: input.onRevisionActiveChange, |
| 125 | onAnswer: prompts.handleApprovalAnswer, |
| 126 | onResolveRecovery: prompts.handleRecoveryAnswer, |
| 127 | onRevisePlan: prompts.handleRevisePlan, |
| 128 | onExitPlan: prompts.handleExitPlan, |
| 129 | onStop: input.onStop, |
| 130 | toolApprovalMode: view.toolApprovalMode, |
| 131 | }, |
| 132 | }; |
| 133 | } |
| 134 | if (surface === "ask" && view.ask) { |
| 135 | return { |
| 136 | kind: "ask", |
| 137 | identity: prompts.questionTarget?.instanceKey ?? `${activeTabId ?? ""}:${view.ask.id}`, |
| 138 | props: { |
| 139 | ask: view.ask, |
| 140 | draftScope: prompts.questionTarget?.instanceKey ?? `${activeTabId ?? ""}:${view.ask.id}`, |
| 141 | onAnswer: prompts.handleQuestionAnswer, |
| 142 | onDismiss: prompts.handleQuestionDismiss, |
| 143 | onStop: input.onStop, |
| 144 | }, |
| 145 | }; |
| 146 | } |
| 147 | if (surface === "mcp_interaction" && view.mcpInteraction) { |
| 148 | return { |
| 149 | kind: "mcp", |
| 150 | identity: prompts.mcpTarget?.instanceKey ?? `${activeTabId ?? ""}:${view.mcpInteraction.id}`, |
| 151 | props: { |
| 152 | interaction: view.mcpInteraction, |
| 153 | instanceKey: prompts.mcpTarget?.instanceKey ?? `${activeTabId ?? ""}:${view.mcpInteraction.id}`, |
| 154 | busy: false, |
| 155 | onAnswer: prompts.handleMCPAnswer, |
| 156 | onOpenLink: input.onOpenLink, |
| 157 | }, |
| 158 | }; |
| 159 | } |
| 160 | if (surface === "extension_form" && view.extensionForm) { |
| 161 | return { |
| 162 | kind: "extension", |
| 163 | identity: `${activeTabId ?? ""}:${view.extensionForm.pluginId}:${view.extensionForm.surfaceId}:${view.extensionForm.formInstanceId}`, |
| 164 | props: { |
| 165 | surface: view.extensionForm, |
| 166 | busy: extension.extensionFormBusy, |
| 167 | onSubmit: (values) => void extension.submitExtensionForm(values), |
| 168 | onCancel: () => void extension.cancelExtensionForm(), |
| 169 | }, |
| 170 | }; |
| 171 | } |
| 172 | if (surface === "workspace_conflict" && view.workspaceConflict) { |
| 173 | const workspaceConflict = view.workspaceConflict; |
| 174 | return { |
| 175 | kind: "runtime", |
| 176 | identity: "workspace-conflict", |
| 177 | props: { |
| 178 | id: "workspace-conflict", |
| 179 | title: t("runtime.workspaceConflictTitle"), |
| 180 | badge: t("runtime.workspaceConflictBadge"), |
| 181 | meta: workspaceConflict.state === "local" |
| 182 | ? t("runtime.workspaceConflictLocal", { title: workspaceConflict.ownerTitle || t("runtime.unknownTask"), label: workspaceConflict.ownerLabel || t("workspace.title") }) |
| 183 | : t("runtime.workspaceConflictExternal"), |
| 184 | note: t("runtime.workspaceConflictNote"), |
| 185 | onCancel: input.cancelWorkspaceConflict, |
| 186 | actions: [ |
| 187 | ...(workspaceConflict.canReveal ? [{ |
| 188 | key: "1", label: t("runtime.revealWriter"), description: t("runtime.revealWriterDesc"), |
| 189 | onClick: () => void tabs.revealWorkspaceWriter(), |
| 190 | }] : []), |
| 191 | ...(workspaceConflict.canCreateWorktree ? [{ |
| 192 | key: "2", label: t("runtime.openWorktree"), description: t("runtime.openWorktreeDesc"), |
| 193 | onClick: () => void tabs.continueInDeliveryWorktree(), |
| 194 | }] : []), |
| 195 | ], |
| 196 | secondaryAction: { |
| 197 | key: "Esc", label: t("runtime.cancelWait"), description: t("runtime.cancelWaitDesc"), |
| 198 | onClick: input.cancelWorkspaceConflict, |
| 199 | }, |
| 200 | }, |
| 201 | }; |
| 202 | } |
| 203 | if (surface === "close_active" && tabs.pendingClose) { |
| 204 | const pendingClose = tabs.pendingClose; |
| 205 | return { |
| 206 | kind: "runtime", |
| 207 | identity: "close-active", |
| 208 | props: { |
| 209 | id: "close-active", |
| 210 | title: t("runtime.closeTitle"), |
| 211 | badge: t("status.jobs", { n: pendingClose.work.jobs.length }), |
| 212 | meta: t("runtime.closeMeta"), |
| 213 | onCancel: () => tabs.setPendingClose(null), |
| 214 | actions: [ |
| 215 | { |
| 216 | key: "1", label: t("runtime.keepRunning"), description: t("runtime.keepRunningDesc"), |
| 217 | onClick: () => void tabs.resolvePendingClose("keep_running"), disabled: pendingClose.stopping, |
| 218 | }, |
| 219 | { |
| 220 | key: "2", label: pendingClose.stopping ? t("status.jobStopping") : t("runtime.stopAndClose"), |
| 221 | description: t("runtime.stopAndCloseDesc"), onClick: () => void tabs.resolvePendingClose("stop_and_close"), |
| 222 | danger: true, disabled: pendingClose.stopping, |
| 223 | }, |
| 224 | ], |
| 225 | secondaryAction: { |
| 226 | key: "Esc", label: t("runtime.returnToTask"), description: t("runtime.closeCancelDesc"), |
| 227 | onClick: () => tabs.setPendingClose(null), disabled: pendingClose.stopping, |
| 228 | }, |
| 229 | }, |
| 230 | }; |
| 231 | } |
| 232 | if (surface === "clear_context") { |
| 233 | return { |
| 234 | kind: "clear-context", |
| 235 | identity: "clear-context", |
| 236 | props: { onCancel: clear.cancelClearContext, onConfirm: () => void clear.confirmClearContext() }, |
| 237 | }; |
| 238 | } |
| 239 | return undefined; |
| 240 | } |
| 241 | |
| 242 | export type ComposerSurfaceInput = { |
| 243 | view: { |
| 244 | hidden: boolean; |
| 245 | inert: boolean; |
| 246 | hero: boolean; |
| 247 | headline: string; |
| 248 | remote: boolean; |
| 249 | rewindCommitting: boolean; |
| 250 | messageActionPending: boolean; |
| 251 | decisionActive: boolean; |
| 252 | runtimeTransitioning: boolean; |
| 253 | controllerReady: boolean; |
| 254 | showContextWindowRing: boolean; |
| 255 | submitDisabledReason?: string; |
| 256 | draftHint?: string; |
| 257 | }; |
| 258 | base: ComposerBase; |
| 259 | tab: { readOnly?: boolean; sessionPath?: string; workspaceRoot?: string; authentication?: ComposerProps["authentication"]; modelSettingsPending?: boolean; remote?: { hostId: string; workspace: string } } | undefined; |
| 260 | tabId: string | undefined; |
| 261 | profile: ReturnType<typeof useComposerProfileProjection>; |
| 262 | router: { handleSend: ComposerProps["onSend"]; handleSteer: ComposerProps["onSteer"] }; |
| 263 | modes: ReturnType<typeof useComposerModeActions>; |
| 264 | goals: ReturnType<typeof useComposerGoalCommands>; |
| 265 | remoteGoal: ReturnType<typeof useRemoteComposerRuntimeActions>; |
| 266 | modelSwitch: Pick<ReturnType<typeof useControllerProfileCommands>, "switchModelFromUi">; |
| 267 | inserts: Pick<ReturnType<typeof useComposerInsertCommands>, "composerInsertRequest" | "selectedTextRequest">; |
| 268 | control: { handleCancelActive: ComposerProps["onCancel"] }; |
| 269 | remoteComposer: { |
| 270 | send: ComposerProps["onSend"]; |
| 271 | cancel: ComposerProps["onCancel"]; |
| 272 | ready: boolean; |
| 273 | profileReady: boolean; |
| 274 | liveStore: ComposerProps["liveStore"]; |
| 275 | }; |
| 276 | localLiveStore: ComposerProps["liveStore"]; |
| 277 | onInvocationMetadataChange: ComposerProps["onInvocationMetadataChange"]; |
| 278 | onCycleMode: ComposerProps["onCycleMode"]; |
| 279 | transientDismissSignal: ComposerProps["transientDismissSignal"]; |
| 280 | sessionKey: ComposerProps["sessionKey"]; |
| 281 | workspaceScopeKey: ComposerProps["workspaceScopeKey"]; |
| 282 | workspaceContext: ComposerProps["workspaceContext"]; |
| 283 | fileRefRefreshKey: ComposerProps["fileRefRefreshKey"]; |
| 284 | guidance: { key: string; itemId?: string; text: string } | null; |
| 285 | guidanceQueuePreviewItems: ComposerProps["guidanceQueuePreviewItems"]; |
| 286 | draft?: ReturnType<typeof useSessionDraftSurface>; |
| 287 | }; |
| 288 | |
| 289 | export function buildComposerSurface(input: ComposerSurfaceInput): DecisionFooterRegionProps["composer"] { |
| 290 | const { base, view, profile, router, modes, goals, remoteGoal, modelSwitch, inserts, control, remoteComposer } = input; |
| 291 | const surface: DecisionFooterRegionProps["composer"] = { |
| 292 | hidden: view.hidden, |
| 293 | inert: view.inert, |
| 294 | hero: view.hero, |
| 295 | headline: view.headline, |
| 296 | props: { |
| 297 | ...base, |
| 298 | running: base.running || (!view.remote && view.rewindCommitting), |
| 299 | collaborationMode: profile.collaborationMode, |
| 300 | toolApprovalMode: profile.toolApprovalMode, |
| 301 | goal: profile.goal, |
| 302 | tabId: input.tabId, |
| 303 | workspaceRoot: input.tab?.workspaceRoot, |
| 304 | onSend: view.remote ? remoteComposer.send : router.handleSend, |
| 305 | onInvocationMetadataChange: input.onInvocationMetadataChange, |
| 306 | onSteer: router.handleSteer, |
| 307 | onCancel: view.remote ? remoteComposer.cancel : control.handleCancelActive, |
| 308 | onCycleMode: input.onCycleMode, |
| 309 | onSetMode: modes.applyMode, |
| 310 | onSetCollaborationMode: goals.setCollaborationModeFromUi, |
| 311 | onSetToolApprovalMode: modes.applyToolApprovalMode, |
| 312 | onClearGoal: goals.clearGoalFromUi, |
| 313 | onEditGoal: goals.editGoalFromUi, |
| 314 | onPauseGoal: remoteGoal.pauseGoal, |
| 315 | onResumeGoal: remoteGoal.resumeGoal, |
| 316 | onSwitchModel: modelSwitch.switchModelFromUi, |
| 317 | onSetEffort: remoteGoal.setEffort, |
| 318 | insertRequest: inserts.composerInsertRequest, |
| 319 | selectedTextRequest: inserts.selectedTextRequest, |
| 320 | readOnly: Boolean(input.tab?.readOnly), |
| 321 | disabled: view.runtimeTransitioning || view.rewindCommitting || view.messageActionPending || view.decisionActive, |
| 322 | submitDisabled: view.remote ? !remoteComposer.ready || !remoteComposer.profileReady : !modelSettingsAllowSubmission(view.controllerReady, input.tab), |
| 323 | submitDisabledReason: view.submitDisabledReason, |
| 324 | authentication: view.remote ? undefined : input.tab?.authentication, |
| 325 | decisionPending: view.rewindCommitting || view.messageActionPending || view.decisionActive, |
| 326 | ready: view.remote ? remoteComposer.ready && remoteComposer.profileReady : view.controllerReady, |
| 327 | liveStore: view.remote ? remoteComposer.liveStore : input.localLiveStore, |
| 328 | suspendedByDecision: view.decisionActive, |
| 329 | transientDismissSignal: input.transientDismissSignal, |
| 330 | sessionKey: input.sessionKey, |
| 331 | inboxSessionPath: input.tab?.sessionPath, |
| 332 | inboxHostId: input.tab?.remote?.hostId, |
| 333 | inboxWorkspace: input.tab?.remote?.workspace, |
| 334 | workspaceScopeKey: input.workspaceScopeKey, |
| 335 | // Match the workspace launcher's ownership boundary: project/branch |
| 336 | // selection configures a new empty session. Once the transcript has |
| 337 | // content, the session keeps its established workspace and the composer |
| 338 | // returns to the compact follow-up layout. |
| 339 | workspaceContext: view.hero || input.draft?.surface ? input.workspaceContext : undefined, |
| 340 | fileRefRefreshKey: input.fileRefRefreshKey, |
| 341 | guidanceConsumedKey: input.guidance?.key, |
| 342 | guidanceConsumedItemId: input.guidance?.itemId, |
| 343 | guidanceConsumedText: input.guidance?.text, |
| 344 | guidanceQueuePreviewItems: input.guidanceQueuePreviewItems, |
| 345 | showContextWindowRing: view.showContextWindowRing, |
| 346 | heroMode: view.hero && view.showContextWindowRing, |
| 347 | }, |
| 348 | }; |
| 349 | const draft = input.draft?.surface; |
| 350 | if (!draft || !input.draft) return surface; |
| 351 | const draftController = input.draft; |
| 352 | const operationActive = draft.preparingSubmission || draftSubmissionLocksEditing(draft.operation); |
| 353 | const needsAttention = draftSurfaceNeedsAttention(draft); |
| 354 | return { |
| 355 | hidden: false, |
| 356 | inert: false, |
| 357 | hero: !needsAttention, |
| 358 | headline: input.view.headline, |
| 359 | hint: needsAttention ? undefined : input.view.draftHint, |
| 360 | props: { |
| 361 | ...surface.props, |
| 362 | running: operationActive && draft.operation?.phase !== "accepted", |
| 363 | collaborationMode: (draft.settings.collaborationMode || "normal") as ComposerProps["collaborationMode"], |
| 364 | toolApprovalMode: (draft.settings.toolApprovalMode || "ask") as ComposerProps["toolApprovalMode"], |
| 365 | goal: draft.settings.goal, |
| 366 | cwd: draft.draft.workspaceRoot, |
| 367 | workspaceRoot: draft.draft.workspaceRoot, |
| 368 | modelLabel: draft.settings.model, |
| 369 | commandCatalog: draft.commands, |
| 370 | tabId: undefined, |
| 371 | onCaptureSubmit: () => draftController.captureSubmission(draft.draft.id, draft.generation), |
| 372 | onReleaseSubmit: draftController.releasePreparation, |
| 373 | onPrepareSubmit: draftController.flushPreparation, |
| 374 | onSend: (display, submit, tabId, structured, capture) => ( |
| 375 | draftController.submitFrom(draft.draft.id, draft.generation, display, submit, tabId, structured, capture) |
| 376 | ), |
| 377 | onSteer: undefined, |
| 378 | onCycleMode: () => draftController.updateSettingsFor(draft.draft.id, draft.generation, { collaborationMode: draft.settings.collaborationMode === "plan" ? "normal" : "plan" }), |
| 379 | readOnly: false, |
| 380 | attachmentInputEnabled: true, |
| 381 | imageInputEnabled: draft.models?.find(model => model.ref === draft.settings.model)?.vision ?? false, |
| 382 | imageUnderstandingEnabled: false, |
| 383 | onCancel: async () => { |
| 384 | await draftController.cancelSubmission(); |
| 385 | return { discardedItemIds: [] }; |
| 386 | }, |
| 387 | onSetMode: (mode) => draftController.updateSettingsFor(draft.draft.id, draft.generation, { mode }), |
| 388 | onSetCollaborationMode: (collaborationMode) => draftController.updateSettingsFor(draft.draft.id, draft.generation, { collaborationMode }), |
| 389 | onSetToolApprovalMode: (toolApprovalMode) => draftController.updateSettingsFor(draft.draft.id, draft.generation, { toolApprovalMode }), |
| 390 | onClearGoal: () => draftController.updateSettingsFor(draft.draft.id, draft.generation, { goal: "", collaborationMode: "normal" }), |
| 391 | onEditGoal: (goal) => draftController.updateSettingsFor(draft.draft.id, draft.generation, { goal, collaborationMode: goal ? "goal" : "normal" }), |
| 392 | onPauseGoal: () => {}, |
| 393 | onResumeGoal: () => {}, |
| 394 | onSwitchModel: (model) => { draftController.updateSettingsFor(draft.draft.id, draft.generation, { model, modelSource: "explicit" }); return true; }, |
| 395 | onSetEffort: (effort) => draftController.updateSettingsFor(draft.draft.id, draft.generation, { effort }), |
| 396 | effort: { |
| 397 | supported: true, |
| 398 | current: draft.settings.effort || "auto", |
| 399 | default: "auto", |
| 400 | levels: ["auto", "low", "medium", "high", "max"], |
| 401 | }, |
| 402 | disabled: operationActive, |
| 403 | submitDisabled: draft.saveState === "conflict" || draft.pendingTasks > 0 || operationActive, |
| 404 | submitDisabledReason: draft.saveState === "conflict" ? "Resolve the draft conflict before sending." : undefined, |
| 405 | decisionPending: operationActive, |
| 406 | ready: true, |
| 407 | liveStore: undefined, |
| 408 | suspendedByDecision: false, |
| 409 | sessionKey: `draft:${draft.draft.id}`, |
| 410 | inboxSessionPath: undefined, |
| 411 | inboxHostId: undefined, |
| 412 | inboxWorkspace: undefined, |
| 413 | workspaceScopeKey: `draft:${draft.draft.workspaceId}`, |
| 414 | workspaceContext: surface.props.workspaceContext ? { |
| 415 | ...surface.props.workspaceContext, |
| 416 | scope: draft.draft.scope === "project" ? "project" : "global", |
| 417 | workspaceRoot: draft.draft.workspaceRoot, |
| 418 | workspaceName: draft.draft.scope === "project" |
| 419 | ? draft.draft.workspaceRoot?.replace(/[\\/]+$/, "").split(/[\\/]/).filter(Boolean).pop() |
| 420 | : undefined, |
| 421 | // Drafts have no formal tab. Git RPCs require that tab's identity and |
| 422 | // must never target the session that happened to be open beforehand. |
| 423 | tabId: undefined, |
| 424 | gitBranch: undefined, |
| 425 | scopeKey: `draft:${draft.draft.workspaceId}`, |
| 426 | remote: false, |
| 427 | } : undefined, |
| 428 | persistentDraft: { |
| 429 | draftId: draft.draft.id, |
| 430 | generation: draft.generation, |
| 431 | initial: draft.content, |
| 432 | revision: draft.draft.revision, |
| 433 | onChange: draftController.updateContentFor, |
| 434 | onPatch: draftController.patchContentFor, |
| 435 | isCurrent: draftController.isCurrentHandle, |
| 436 | canEdit: draftController.canEditHandle, |
| 437 | trackTask: draftController.trackTask, |
| 438 | onTaskError: draftController.reportTaskError, |
| 439 | }, |
| 440 | composerTarget: { kind: "draft", draftId: draft.draft.id }, |
| 441 | }, |
| 442 | }; |
| 443 | } |
| 444 |