refactor(native-chat): run quick create through the shared settle loop

Quick create was the last entrypoint driving the launch handle itself,
because its cancel lifecycle is real: when the creation is abandoned the
structured launch must be cancelled immediately so a staged prompt never
reaches the provider. The shared loop now takes a cancellation hook with an
eager subscription plus a post-await check; it cancels the launch once,
unsubscribes on settle, and reports cancelled without running the fallback.
Quick create keeps its two-branch legacy fallback and retire-on-late-cancel.

Also updates the surface-caller census for the onboarding launch module
that step 2 introduced.
This commit is contained in:
Merge Sim
2026-09-08 19:26:47 -07:00
parent 7d40e6cc09
commit e050c8a9ba
5 changed files with 549 additions and 137 deletions
@@ -1,11 +1,13 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
startStructuredAgentLaunch: vi.fn()
startStructuredAgentLaunch: vi.fn(),
cancelStructuredAgentLaunch: vi.fn()
}))
vi.mock('@/lib/structured-agent-session-launch', () => ({
startStructuredAgentLaunch: mocks.startStructuredAgentLaunch
startStructuredAgentLaunch: mocks.startStructuredAgentLaunch,
cancelStructuredAgentLaunch: mocks.cancelStructuredAgentLaunch
}))
vi.mock('@/lib/launch-structured-agent-session', () => ({
@@ -52,6 +54,29 @@ const fallbackResult = {
primaryTabId: 'fallback-tab'
}
/** A caller-side cancel signal: `fire` is what the caller's store subscription would call. */
function fakeCancellation(initiallyCancelled = false) {
let cancelled = initiallyCancelled
const unsubscribe = vi.fn()
const listeners: (() => void)[] = []
return {
unsubscribe,
fire: () => {
cancelled = true
for (const listener of listeners) {
listener()
}
},
hook: {
isCancelled: () => cancelled,
subscribe: vi.fn((onCancel: () => void) => {
listeners.push(onCancel)
return unsubscribe
})
}
}
}
describe('settleStructuredAgentLaunch', () => {
beforeEach(() => vi.clearAllMocks())
@@ -173,10 +198,10 @@ describe('settleStructuredAgentLaunch', () => {
{},
{
onStructuredReady,
isCancelled: () => true
cancellation: fakeCancellation(true).hook
}
)
).resolves.toEqual({ kind: 'cancelled' })
).resolves.toEqual({ kind: 'cancelled', sessionId: 'session-1' })
expect(onStructuredReady).not.toHaveBeenCalled()
})
@@ -193,10 +218,71 @@ describe('settleStructuredAgentLaunch', () => {
{},
{
legacyFallback,
isCancelled: () => true
cancellation: fakeCancellation(true).hook
}
)
).resolves.toEqual({ kind: 'cancelled' })
).resolves.toEqual({ kind: 'cancelled', sessionId: 'session-1' })
expect(legacyFallback).not.toHaveBeenCalled()
})
it('cancels the launch eagerly, once, before the launch settles', async () => {
let resolveLaunch!: (receipt: { sessionId: string; fence: number }) => void
fakeLaunch({
launchResult: new Promise((resolve) => {
resolveLaunch = resolve
})
})
const cancellation = fakeCancellation()
const onStructuredReady = vi.fn()
const settlement = settleStructuredAgentLaunch(
'worktree-1',
'codex',
{},
{ onStructuredReady, cancellation: cancellation.hook }
)
expect(mocks.cancelStructuredAgentLaunch).not.toHaveBeenCalled()
cancellation.fire()
cancellation.fire()
expect(mocks.cancelStructuredAgentLaunch).toHaveBeenCalledExactlyOnceWith(
'worktree-1',
'session-1'
)
expect(cancellation.unsubscribe).not.toHaveBeenCalled()
resolveLaunch({ sessionId: 'session-1', fence: 1 })
await expect(settlement).resolves.toEqual({ kind: 'cancelled', sessionId: 'session-1' })
expect(onStructuredReady).not.toHaveBeenCalled()
expect(cancellation.unsubscribe).toHaveBeenCalledOnce()
})
it('honours a cancellation that fired before the loop subscribed', async () => {
fakeLaunch({ launchResult: Promise.resolve({ sessionId: 'session-1', fence: 1 }) })
const cancellation = fakeCancellation(true)
const settlement = settleStructuredAgentLaunch(
'worktree-1',
'codex',
{},
{ cancellation: cancellation.hook }
)
expect(cancellation.hook.subscribe).toHaveBeenCalledOnce()
expect(mocks.cancelStructuredAgentLaunch).toHaveBeenCalledExactlyOnceWith(
'worktree-1',
'session-1'
)
await expect(settlement).resolves.toEqual({ kind: 'cancelled', sessionId: 'session-1' })
expect(cancellation.unsubscribe).toHaveBeenCalledOnce()
})
it('unsubscribes from the cancel signal once a launch settles without cancelling', async () => {
fakeLaunch({ launchResult: Promise.resolve({ sessionId: 'session-1', fence: 1 }) })
const cancellation = fakeCancellation()
await expect(
settleStructuredAgentLaunch('worktree-1', 'codex', {}, { cancellation: cancellation.hook })
).resolves.toEqual({ kind: 'structured', sessionId: 'session-1' })
expect(mocks.cancelStructuredAgentLaunch).not.toHaveBeenCalled()
expect(cancellation.unsubscribe).toHaveBeenCalledOnce()
})
})
@@ -1,6 +1,7 @@
import type { AgentSessionHandleProvider } from '../../../shared/agent-session-provider-handle'
import { StructuredAgentSessionCreateRefusalError } from '@/lib/launch-structured-agent-session'
import {
cancelStructuredAgentLaunch,
startStructuredAgentLaunch,
type StructuredAgentLaunchOptions
} from '@/lib/structured-agent-session-launch'
@@ -21,17 +22,24 @@ export type StructuredAgentLaunchSettlement =
promptDeliveryResult?: Promise<StructuredPromptDeliveryResult>
}
| ({ kind: 'refused-then-legacy' } & StructuredAgentLegacyFallbackResult)
| { kind: 'cancelled' }
| { kind: 'cancelled'; sessionId: string }
| { kind: 'visibility-unknown'; sessionId: string }
| { kind: 'failed'; error: unknown }
export type StructuredAgentLaunchCancellation = {
isCancelled: () => boolean
/** Fires the moment the caller abandons the launch. The loop cancels eagerly on it so a staged
* prompt is discarded before it can reach the provider; a token read only after awaits is late. */
subscribe: (onCancel: () => void) => () => void
}
export type StructuredAgentLaunchHooks = {
/** What this flow did before structured chat existed: activate with a startup payload, set the
* first-message rename flag, run trust preflight. Runs at most once, only on definitive refusal.
* Resume has no legacy equivalent, so a refusal without this hook settles as `failed`. */
legacyFallback?: () => Promise<StructuredAgentLegacyFallbackResult>
onStructuredReady?: (sessionId: string) => void
isCancelled?: () => boolean
cancellation?: StructuredAgentLaunchCancellation
}
/**
@@ -45,8 +53,21 @@ export async function settleStructuredAgentLaunch(
options: StructuredAgentLaunchOptions,
hooks: StructuredAgentLaunchHooks
): Promise<StructuredAgentLaunchSettlement> {
const isCancelled = (): boolean => hooks.isCancelled?.() === true
const launch = startStructuredAgentLaunch(worktreeId, agent, options)
let cancelRequested = false
const isCancelled = (): boolean => cancelRequested || hooks.cancellation?.isCancelled() === true
const cancelLaunch = (): void => {
if (cancelRequested) {
return
}
cancelRequested = true
cancelStructuredAgentLaunch(worktreeId, launch.sessionId)
}
const unsubscribe = hooks.cancellation?.subscribe(cancelLaunch)
// Why: the caller may have been abandoned between its own check and this subscription.
if (isCancelled()) {
cancelLaunch()
}
// Why: a holder, not a `let`: TS narrows a closure-assigned local to its initial null.
const fallback: { result: StructuredAgentLegacyFallbackResult | null } = { result: null }
// Why: the claim resolves after the callback settles, so awaiting it below is what serialises
@@ -58,10 +79,14 @@ export async function settleStructuredAgentLaunch(
}
fallback.result = await hooks.legacyFallback()
})
const cancelled = (): StructuredAgentLaunchSettlement => ({
kind: 'cancelled',
sessionId: launch.sessionId
})
try {
const receipt = await launch.launchResult
if (isCancelled()) {
return { kind: 'cancelled' }
return cancelled()
}
hooks.onStructuredReady?.(receipt.sessionId)
return {
@@ -71,7 +96,7 @@ export async function settleStructuredAgentLaunch(
}
} catch (error) {
if (isCancelled()) {
return { kind: 'cancelled' }
return cancelled()
}
if (error instanceof StructuredAgentSessionCreateRefusalError) {
const ran = await refusalFallback.then(
@@ -79,7 +104,7 @@ export async function settleStructuredAgentLaunch(
(fallbackError: unknown) => ({ fallbackError })
)
if (isCancelled()) {
return { kind: 'cancelled' }
return cancelled()
}
if (typeof ran !== 'boolean') {
return { kind: 'failed', error: ran.fallbackError }
@@ -96,5 +121,7 @@ export async function settleStructuredAgentLaunch(
return { kind: 'visibility-unknown', sessionId: launch.sessionId }
}
return { kind: 'failed', error }
} finally {
unsubscribe?.()
}
}
@@ -10,7 +10,6 @@ import { describe, expect, it } from 'vitest'
const SURFACE_PROVIDING_CALLERS = [
'src/renderer/src/components/editor/check-annotation-open.ts',
'src/renderer/src/components/feature-wall/FeatureWallBrowserAction.tsx',
'src/renderer/src/components/sidebar/NonGitFolderDialog.tsx',
'src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts',
'src/renderer/src/components/sidebar/run-worktree-delete-with-toast.ts',
'src/renderer/src/components/terminal-pane/terminal-file-open-routing.ts',
@@ -19,7 +18,7 @@ const SURFACE_PROVIDING_CALLERS = [
'src/renderer/src/lib/launch-work-item-direct.ts',
'src/renderer/src/lib/worktree-creation-flow-execute.ts',
'src/renderer/src/lib/workspace-port-actions.ts',
'src/renderer/src/store/repos/repo-add-actions.ts'
'src/renderer/src/lib/onboarding-folder-agent-launch.ts'
]
// The activation seam itself: declares the option and forwards it into the tombstone gate.
@@ -10,7 +10,11 @@ const mocks = vi.hoisted(() => ({
cancelStructuredAgentLaunch: vi.fn(),
closeStructuredAgentSession: vi.fn(),
callRuntimeRpc: vi.fn(),
activateStructuredAgentSessionById: vi.fn()
activateStructuredAgentSessionById: vi.fn(),
activateAndRevealWorktree: vi.fn(),
ensureWorktreeHasInitialTerminal: vi.fn(),
preflightAgentTrust: vi.fn(),
updateWorktreeMeta: vi.fn()
}))
vi.mock('@/store', () => ({
@@ -47,23 +51,65 @@ vi.mock('@/lib/structured-agent-session-tab-activation', () => ({
}))
vi.mock('@/lib/worktree-initial-terminal-seeding', () => ({
ensureWorktreeHasInitialTerminal: vi.fn()
ensureWorktreeHasInitialTerminal: mocks.ensureWorktreeHasInitialTerminal
}))
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealWorktree: vi.fn()
activateAndRevealWorktree: mocks.activateAndRevealWorktree
}))
vi.mock('@/lib/agent-trust-preflight', () => ({
preflightAgentTrust: vi.fn()
preflightAgentTrust: mocks.preflightAgentTrust
}))
vi.mock('@/lib/launch-structured-agent-session', () => ({
StructuredAgentSessionCreateRefusalError: class extends Error {}
}))
import { StructuredAgentSessionCreateRefusalError } from '@/lib/launch-structured-agent-session'
import { launchStructuredWorktreeSession } from './worktree-creation-structured-session'
const request = {
repoId: 'repo-1',
name: 'routing-recovery',
setupDecision: 'run' as const,
agent: 'codex' as const,
pendingFirstAgentMessageRename: true,
note: '',
startupPlan: null,
quickPrompt: 'Fix the route',
quickTelemetry: null
}
const idle = { accepted: true, cancelled: false, visibilityUnknown: false }
/** Mirrors the callers layer: a refusal runs the claimed fallback once and resolves true. */
function refusedLaunch(sessionId = 'session-refused') {
const launchResult = Promise.reject(new StructuredAgentSessionCreateRefusalError('unsupported'))
mocks.startStructuredAgentLaunch.mockReturnValue({
sessionId,
launchResult,
isVisibilityUnknown: () => false,
releaseCallerAfterUnknownOutcome: vi.fn(),
claimDefinitiveRefusalFallback: vi.fn((fallback: () => Promise<void>) =>
launchResult.catch(() =>
Promise.resolve()
.then(fallback)
.then(() => true)
)
)
})
}
function storeWithWorktree() {
mocks.state = {
pendingWorktreeCreations: { 'creation-1': {} },
allWorktrees: () => [{ id: 'worktree-1', path: '/tmp/worktree-1' }],
repos: [{ id: 'repo-1', connectionId: 'ssh-1' }],
updateWorktreeMeta: mocks.updateWorktreeMeta
} as unknown as typeof mocks.state
}
describe('launchStructuredWorktreeSession', () => {
beforeEach(() => {
vi.clearAllMocks()
@@ -71,6 +117,259 @@ describe('launchStructuredWorktreeSession', () => {
mocks.listener = null
mocks.closeStructuredAgentSession.mockResolvedValue('closed')
mocks.callRuntimeRpc.mockResolvedValue(undefined)
mocks.updateWorktreeMeta.mockResolvedValue(undefined)
mocks.preflightAgentTrust.mockResolvedValue(undefined)
})
it('activates the structured session once it is published', async () => {
mocks.startStructuredAgentLaunch.mockReturnValue({
sessionId: 'session-1',
launchResult: Promise.resolve({ sessionId: 'session-1', fence: 1 }),
isVisibilityUnknown: () => false,
releaseCallerAfterUnknownOutcome: vi.fn(),
claimDefinitiveRefusalFallback: vi.fn(() => Promise.resolve(false))
})
await expect(
launchStructuredWorktreeSession({
creationId: 'creation-1',
request,
worktreeId: 'worktree-1',
shouldActivateOnCompletion: true,
fallbackStartupOpt: undefined,
activation: false,
primaryTabId: null
})
).resolves.toEqual({ ...idle, activation: false, primaryTabId: null })
expect(mocks.startStructuredAgentLaunch).toHaveBeenCalledWith('worktree-1', 'codex', {
prompt: 'Fix the route'
})
expect(mocks.activateStructuredAgentSessionById).toHaveBeenCalledExactlyOnceWith({
worktreeId: 'worktree-1',
sessionId: 'session-1'
})
expect(mocks.cancelStructuredAgentLaunch).not.toHaveBeenCalled()
expect(mocks.unsubscribe).toHaveBeenCalledOnce()
})
it('does not activate a published session when the user has moved on', async () => {
mocks.startStructuredAgentLaunch.mockReturnValue({
sessionId: 'session-1',
launchResult: Promise.resolve({ sessionId: 'session-1', fence: 1 }),
isVisibilityUnknown: () => false,
releaseCallerAfterUnknownOutcome: vi.fn(),
claimDefinitiveRefusalFallback: vi.fn(() => Promise.resolve(false))
})
await launchStructuredWorktreeSession({
creationId: 'creation-1',
request,
worktreeId: 'worktree-1',
shouldActivateOnCompletion: false,
fallbackStartupOpt: undefined,
activation: false,
primaryTabId: 'tab-existing'
})
expect(mocks.activateStructuredAgentSessionById).not.toHaveBeenCalled()
})
it('retries an unknown launch with no prompt so the outbox is not re-staged', async () => {
mocks.startStructuredAgentLaunch.mockReturnValue({
sessionId: 'session-1',
launchResult: Promise.resolve({ sessionId: 'session-1', fence: 1 }),
isVisibilityUnknown: () => false,
releaseCallerAfterUnknownOutcome: vi.fn(),
claimDefinitiveRefusalFallback: vi.fn(() => Promise.resolve(false))
})
await launchStructuredWorktreeSession({
creationId: 'creation-1',
request,
worktreeId: 'worktree-1',
shouldActivateOnCompletion: true,
fallbackStartupOpt: undefined,
activation: false,
primaryTabId: null,
recoverUnknownLaunch: true
})
expect(mocks.startStructuredAgentLaunch).toHaveBeenCalledWith('worktree-1', 'codex', {})
})
it('returns cancelled without starting a launch when the creation is already gone', async () => {
mocks.state = { pendingWorktreeCreations: {} }
await expect(
launchStructuredWorktreeSession({
creationId: 'creation-1',
request,
worktreeId: 'worktree-1',
shouldActivateOnCompletion: true,
fallbackStartupOpt: undefined,
activation: false,
primaryTabId: null
})
).resolves.toEqual({ ...idle, cancelled: true, activation: false, primaryTabId: null })
expect(mocks.startStructuredAgentLaunch).not.toHaveBeenCalled()
expect(mocks.closeStructuredAgentSession).not.toHaveBeenCalled()
})
it('discards the launch eagerly when the creation is dismissed while it is pending', async () => {
const launchResult = new Promise<never>(() => {})
mocks.startStructuredAgentLaunch.mockReturnValue({
sessionId: 'session-1',
launchResult,
isVisibilityUnknown: () => false,
releaseCallerAfterUnknownOutcome: vi.fn(),
claimDefinitiveRefusalFallback: vi.fn(() => launchResult)
})
void launchStructuredWorktreeSession({
creationId: 'creation-1',
request,
worktreeId: 'worktree-1',
shouldActivateOnCompletion: true,
fallbackStartupOpt: undefined,
activation: false,
primaryTabId: null
})
await Promise.resolve()
expect(mocks.cancelStructuredAgentLaunch).not.toHaveBeenCalled()
mocks.state = { pendingWorktreeCreations: {} }
mocks.listener?.(mocks.state)
mocks.listener?.(mocks.state)
expect(mocks.cancelStructuredAgentLaunch).toHaveBeenCalledExactlyOnceWith(
'worktree-1',
'session-1'
)
})
it('falls back to an activated terminal after a definitive refusal', async () => {
storeWithWorktree()
refusedLaunch()
mocks.activateAndRevealWorktree.mockReturnValue({ primaryTabId: 'terminal-tab' })
const startup = { kind: 'agent', agent: 'codex', prompt: 'Fix the route' }
await expect(
launchStructuredWorktreeSession({
creationId: 'creation-1',
request,
worktreeId: 'worktree-1',
shouldActivateOnCompletion: true,
fallbackStartupOpt: startup as never,
activation: false,
primaryTabId: null
})
).resolves.toEqual({
...idle,
accepted: false,
activation: { primaryTabId: 'terminal-tab' },
primaryTabId: 'terminal-tab'
})
expect(mocks.updateWorktreeMeta).toHaveBeenCalledWith('worktree-1', {
pendingFirstAgentMessageRename: true
})
expect(mocks.preflightAgentTrust).toHaveBeenCalledWith({
agent: 'codex',
workspacePath: '/tmp/worktree-1',
connectionId: 'ssh-1'
})
expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith('worktree-1', {
sidebarRevealBehavior: 'auto',
createNewTerminalForStartup: true,
startup
})
expect(mocks.ensureWorktreeHasInitialTerminal).not.toHaveBeenCalled()
expect(mocks.activateStructuredAgentSessionById).not.toHaveBeenCalled()
expect(mocks.closeStructuredAgentSession).not.toHaveBeenCalled()
expect(mocks.unsubscribe).toHaveBeenCalledOnce()
})
it('seeds a background terminal after a refusal when the user has moved on', async () => {
storeWithWorktree()
refusedLaunch()
mocks.ensureWorktreeHasInitialTerminal.mockReturnValue('background-tab')
await expect(
launchStructuredWorktreeSession({
creationId: 'creation-1',
request,
worktreeId: 'worktree-1',
shouldActivateOnCompletion: false,
fallbackStartupOpt: undefined,
activation: false,
primaryTabId: null
})
).resolves.toEqual({
...idle,
accepted: false,
activation: false,
primaryTabId: 'background-tab'
})
expect(mocks.activateAndRevealWorktree).not.toHaveBeenCalled()
expect(mocks.ensureWorktreeHasInitialTerminal).toHaveBeenCalledWith(
mocks.state,
'worktree-1',
undefined,
undefined,
undefined,
undefined,
{ activateCreatedTabs: false, createNewTerminalForStartup: true }
)
})
it('stops the fallback mid-way when the creation is dismissed and retires nothing', async () => {
storeWithWorktree()
refusedLaunch()
mocks.preflightAgentTrust.mockImplementation(async () => {
mocks.state = { ...mocks.state, pendingWorktreeCreations: {} }
mocks.listener?.(mocks.state)
})
await expect(
launchStructuredWorktreeSession({
creationId: 'creation-1',
request,
worktreeId: 'worktree-1',
shouldActivateOnCompletion: true,
fallbackStartupOpt: undefined,
activation: false,
primaryTabId: null
})
).resolves.toEqual({
...idle,
accepted: false,
cancelled: true,
activation: false,
primaryTabId: null
})
expect(mocks.activateAndRevealWorktree).not.toHaveBeenCalled()
expect(mocks.closeStructuredAgentSession).not.toHaveBeenCalled()
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
})
it('reports a known failure as accepted with the caller surface untouched', async () => {
mocks.startStructuredAgentLaunch.mockReturnValue({
sessionId: 'session-1',
launchResult: Promise.reject(new Error('boom')),
isVisibilityUnknown: () => false,
releaseCallerAfterUnknownOutcome: vi.fn(),
claimDefinitiveRefusalFallback: vi.fn(() => Promise.resolve(false))
})
await expect(
launchStructuredWorktreeSession({
creationId: 'creation-1',
request,
worktreeId: 'worktree-1',
shouldActivateOnCompletion: true,
fallbackStartupOpt: undefined,
activation: { primaryTabId: 'tab-1' } as never,
primaryTabId: 'tab-1'
})
).resolves.toEqual({ ...idle, activation: { primaryTabId: 'tab-1' }, primaryTabId: 'tab-1' })
expect(mocks.activateStructuredAgentSessionById).not.toHaveBeenCalled()
expect(mocks.activateAndRevealWorktree).not.toHaveBeenCalled()
})
it('cancels and retires a session when its pending creation is dismissed', async () => {
@@ -2,10 +2,9 @@ import { useAppStore } from '@/store'
import { ensureWorktreeHasInitialTerminal } from '@/lib/worktree-initial-terminal-seeding'
import { activateAndRevealWorktree, type ActivateAndRevealResult } from '@/lib/worktree-activation'
import {
cancelStructuredAgentLaunch,
startStructuredAgentLaunch
} from '@/lib/structured-agent-session-launch'
import { StructuredAgentSessionCreateRefusalError } from '@/lib/launch-structured-agent-session'
settleStructuredAgentLaunch,
type StructuredAgentLegacyFallbackResult
} from '@/lib/structured-agent-launch-settlement'
import { isAgentSessionHandleProvider } from '../../../shared/agent-session-provider-handle'
import { activateStructuredAgentSessionById } from '@/lib/structured-agent-session-tab-activation'
import { preflightAgentTrust } from '@/lib/agent-trust-preflight'
@@ -23,6 +22,17 @@ export type WorktreeCreationStructuredSessionResult = {
primaryTabId: string | null
}
type LaunchStructuredWorktreeSessionArgs = {
creationId: string
request: WorktreeCreationRequest
worktreeId: string
shouldActivateOnCompletion: boolean
fallbackStartupOpt: WorktreeStartupPayload | undefined
activation: ActivateAndRevealResult | false
primaryTabId: string | null
recoverUnknownLaunch?: boolean
}
async function retireCancelledStructuredSession(
worktreeId: string,
sessionId: string
@@ -36,95 +46,48 @@ async function retireCancelledStructuredSession(
}).catch(() => undefined)
}
export async function launchStructuredWorktreeSession(args: {
creationId: string
request: WorktreeCreationRequest
worktreeId: string
shouldActivateOnCompletion: boolean
fallbackStartupOpt: WorktreeStartupPayload | undefined
activation: ActivateAndRevealResult | false
primaryTabId: string | null
recoverUnknownLaunch?: boolean
}): Promise<WorktreeCreationStructuredSessionResult> {
let { activation, primaryTabId } = args
let accepted = true
let visibilityUnknown = false
const agent = args.request.agent
if (!isAgentSessionHandleProvider(agent)) {
return { accepted, cancelled: false, visibilityUnknown, activation, primaryTabId }
}
if (!useAppStore.getState().pendingWorktreeCreations[args.creationId]) {
return { accepted, cancelled: true, visibilityUnknown, activation, primaryTabId }
}
const launch = startStructuredAgentLaunch(
args.worktreeId,
agent,
args.recoverUnknownLaunch
? {}
: {
prompt: args.request.launchDraftPrompt ?? args.request.quickPrompt,
...(args.request.launchDraftPrompt ? { promptDelivery: 'draft' as const } : {})
}
)
let cancelled = false
const cancelLaunch = (): void => {
if (cancelled) {
return
}
cancelled = true
cancelStructuredAgentLaunch(args.worktreeId, launch.sessionId)
}
const unsubscribe = useAppStore.subscribe((state) => {
if (!state.pendingWorktreeCreations[args.creationId]) {
cancelLaunch()
}
})
if (!useAppStore.getState().pendingWorktreeCreations[args.creationId]) {
cancelLaunch()
}
const refusalFallback = launch.claimDefinitiveRefusalFallback(async () => {
accepted = false
if (cancelled) {
return
}
if (args.request.pendingFirstAgentMessageRename) {
await useAppStore
.getState()
.updateWorktreeMeta(args.worktreeId, { pendingFirstAgentMessageRename: true })
.catch(() => undefined)
}
if (cancelled) {
return
}
const worktree = useAppStore
/** What quick create did before structured chat: rename flag, trust preflight, then a terminal. */
async function openLegacyWorktreeSurface(
args: LaunchStructuredWorktreeSessionArgs,
isCancelled: () => boolean
): Promise<StructuredAgentLegacyFallbackResult> {
const unchanged = { activation: args.activation, primaryTabId: args.primaryTabId }
if (args.request.pendingFirstAgentMessageRename) {
await useAppStore
.getState()
.allWorktrees?.()
.find((candidate) => candidate.id === args.worktreeId)
if (args.request.agent && worktree?.path) {
const repoConnectionId = useAppStore
.getState()
.repos.find((repo) => repo.id === args.request.repoId)?.connectionId
await preflightAgentTrust({
agent: args.request.agent,
workspacePath: worktree.path,
connectionId: repoConnectionId
})
}
if (cancelled) {
return
}
if (args.shouldActivateOnCompletion) {
const fallbackActivation = activateAndRevealWorktree(args.worktreeId, {
sidebarRevealBehavior: 'auto',
createNewTerminalForStartup: true,
...(args.fallbackStartupOpt ? { startup: args.fallbackStartupOpt } : {})
})
activation = fallbackActivation
primaryTabId = fallbackActivation === false ? null : fallbackActivation.primaryTabId
return
}
primaryTabId = ensureWorktreeHasInitialTerminal(
.updateWorktreeMeta(args.worktreeId, { pendingFirstAgentMessageRename: true })
.catch(() => undefined)
}
if (isCancelled()) {
return unchanged
}
const worktree = useAppStore
.getState()
.allWorktrees?.()
.find((candidate) => candidate.id === args.worktreeId)
if (args.request.agent && worktree?.path) {
const repoConnectionId = useAppStore
.getState()
.repos.find((repo) => repo.id === args.request.repoId)?.connectionId
await preflightAgentTrust({
agent: args.request.agent,
workspacePath: worktree.path,
connectionId: repoConnectionId
})
}
if (isCancelled()) {
return unchanged
}
if (args.shouldActivateOnCompletion) {
const activation = activateAndRevealWorktree(args.worktreeId, {
sidebarRevealBehavior: 'auto',
createNewTerminalForStartup: true,
...(args.fallbackStartupOpt ? { startup: args.fallbackStartupOpt } : {})
})
return { activation, primaryTabId: activation === false ? null : activation.primaryTabId }
}
return {
primaryTabId: ensureWorktreeHasInitialTerminal(
useAppStore.getState(),
args.worktreeId,
args.fallbackStartupOpt,
@@ -133,35 +96,73 @@ export async function launchStructuredWorktreeSession(args: {
undefined,
{ activateCreatedTabs: false, createNewTerminalForStartup: true }
)
})
}
}
try {
const receipt = await launch.launchResult
if (cancelled) {
await retireCancelledStructuredSession(args.worktreeId, launch.sessionId)
return { accepted, cancelled, visibilityUnknown, activation, primaryTabId }
}
if (args.shouldActivateOnCompletion) {
activateStructuredAgentSessionById({
worktreeId: args.worktreeId,
sessionId: receipt.sessionId
})
}
} catch (error) {
if (cancelled) {
await retireCancelledStructuredSession(args.worktreeId, launch.sessionId)
return { accepted, cancelled, visibilityUnknown, activation, primaryTabId }
}
if (error instanceof StructuredAgentSessionCreateRefusalError) {
await refusalFallback
} else {
visibilityUnknown = launch.isVisibilityUnknown()
if (visibilityUnknown) {
launch.releaseCallerAfterUnknownOutcome()
export async function launchStructuredWorktreeSession(
args: LaunchStructuredWorktreeSessionArgs
): Promise<WorktreeCreationStructuredSessionResult> {
const { activation, primaryTabId } = args
const settled = { accepted: true, cancelled: false, visibilityUnknown: false }
const agent = args.request.agent
if (!isAgentSessionHandleProvider(agent)) {
return { ...settled, activation, primaryTabId }
}
const isCancelled = (): boolean =>
!useAppStore.getState().pendingWorktreeCreations[args.creationId]
if (isCancelled()) {
return { ...settled, cancelled: true, activation, primaryTabId }
}
let refused = false
const settlement = await settleStructuredAgentLaunch(
args.worktreeId,
agent,
args.recoverUnknownLaunch
? {}
: {
prompt: args.request.launchDraftPrompt ?? args.request.quickPrompt,
...(args.request.launchDraftPrompt ? { promptDelivery: 'draft' as const } : {})
},
{
cancellation: {
isCancelled,
subscribe: (onCancel) =>
useAppStore.subscribe((state) => {
if (!state.pendingWorktreeCreations[args.creationId]) {
onCancel()
}
})
},
legacyFallback: () => {
refused = true
return openLegacyWorktreeSurface(args, isCancelled)
},
onStructuredReady: (sessionId) => {
if (args.shouldActivateOnCompletion) {
activateStructuredAgentSessionById({ worktreeId: args.worktreeId, sessionId })
}
}
}
} finally {
unsubscribe()
)
switch (settlement.kind) {
case 'cancelled':
// Why: a refusal means no session exists on the host, so there is nothing to retire.
if (!refused) {
await retireCancelledStructuredSession(args.worktreeId, settlement.sessionId)
}
return { ...settled, accepted: !refused, cancelled: true, activation, primaryTabId }
case 'refused-then-legacy':
return {
...settled,
accepted: false,
activation: settlement.activation ?? activation,
primaryTabId: settlement.primaryTabId
}
case 'visibility-unknown':
return { ...settled, visibilityUnknown: true, activation, primaryTabId }
case 'structured':
case 'failed':
// Why: a failed launch has always reported as accepted here; the launch layer toasts it.
return { ...settled, activation, primaryTabId }
}
return { accepted, cancelled, visibilityUnknown, activation, primaryTabId }
}