Files
orca/src/main/runtime/runtime-folder-worktree-create.ts
T
Jinwoo HongandE2E Test 997d34b043 Layout groundwork (switched off): main creates, starts and closes terminal tabs and panes itself (#31130)
* wip(layout): applyDefaultTabs command in the layout model (dark)

* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one

* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)

* wip(layout): a create or split sent again after its close is refused (dark)

* wip: typed test doubles

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)

* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)

* wip: lint fixes (max-lines, test throws)

* test(layout): a terminal close stands after a real failed profile write; switch off still refuses

* fix(layout): checked types in the close-failure rule and its tests

* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)

* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed

* fix(layout): the switch-off create path gains no await

* fix(layout): a failed start never writes back a pane the user closed (dark)

* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)

* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)

* test(layout): the close fixture answers main's command without naming the method

* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)

* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)

* fix(layout): a pane's start status ends when its pane binds again by any path (dark)

* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design

* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on

An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.

* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1

Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.

* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands

A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.

* fix(terminal): a provider-reported -1 keeps the pane, as the window does

Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.

* chore(terminal): satisfy the exhaustive-switch and test typing checks

* test(terminal): give the replay test's live process a cwd

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 15:01:25 -04:00

243 lines
10 KiB
TypeScript

import { paneIdentity } from './runtime-terminal-pane-identity'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch'
import { shouldSeedWorktreePrimaryTerminal } from '../../shared/worktree-first-surface'
import { randomUUID } from 'node:crypto'
import { getProjectHostSetupWorktreeMeta } from '../../shared/project-host-setup-lookup'
import { resolveWorktreeCreateDisplayNameRequest } from '../ipc/worktree-display-name'
import type { RuntimeTerminalCreate } from '../../shared/runtime-types'
import type { CreateWorktreeResult } from '../../shared/worktree/create-types'
import type { Repo } from '../../shared/repo-types'
import type { TuiAgent } from '../../shared/tui-agent'
import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types'
import type { AgentLaunchRuntime } from '../../shared/agent-launch-runtime'
import {
getRuntimeFolderWorkspaceInstanceId,
mergeRuntimeFolderWorkspace
} from './runtime-folder-workspace'
import type { RuntimeManagedWorktreeCreateArgs } from './runtime-managed-worktree-create-types'
import type { RuntimeStore } from './runtime-store-contract'
import type { TerminalCreateOptions } from './runtime-terminal-contracts'
import type {
WorktreeStartupDraftPaste,
WorktreeStartupFollowup
} from './runtime-worktree-agent-startup'
import { hostWorktreeTargetForRepo } from '../persistence/host-qualified-worktree-meta'
type RuntimeFolderWorktreeCreateDeps = {
store: RuntimeStore
ptySpawnAvailable: boolean
provisionInBackground?: () => boolean
createTerminal: (
selector: string,
options: TerminalCreateOptions
) => Promise<RuntimeTerminalCreate>
pasteDraft: (handle: string, draft: WorktreeStartupDraftPaste) => void
sendFollowup: (handle: string, followup: WorktreeStartupFollowup) => void
invalidateResolvedWorktrees: () => void
notifyWorktreesChanged: (repoId: string) => void
emitCreated: (event: {
kind: 'created'
worktreeId: string
path: string
branch: string
}) => void
activate: (
repoId: string,
worktreeId: string,
setup?: undefined,
startup?: WorktreeStartupLaunch,
providesInitialSurface?: boolean
) => void
}
export async function createRuntimeFolderWorktree(args: {
request: RuntimeManagedWorktreeCreateArgs
repo: Repo
startup?: WorktreeStartupLaunch
startupFollowup?: WorktreeStartupFollowup
/** What a host-built startup was quoted for, or the repair error it fails with. */
startupLaunchRuntime?: AgentLaunchRuntime | Error
createdWithAgent?: TuiAgent
promisedAgentChat?: boolean
onWorkspaceCreated?: (worktreeId: string) => void
draftPaste?: WorktreeStartupDraftPaste
deps: RuntimeFolderWorktreeCreateDeps
}): Promise<CreateWorktreeResult> {
const { request, repo, deps } = args
const now = Date.now()
const settings = deps.store.getSettings()
const instanceId = randomUUID()
const worktreeId = getRuntimeFolderWorkspaceInstanceId(repo, instanceId)
const displayNameRequest = resolveWorktreeCreateDisplayNameRequest(
request.displayName,
request.displayNameKind,
request.name,
request.cliProvenance?.kind === 'created-by-cli',
request.nameWasGenerated === true
)
const resolvedFolderDisplayName = displayNameRequest.value
const meta = deps.store.setWorktreeMeta(hostWorktreeTargetForRepo(worktreeId, repo), {
instanceId,
...getProjectHostSetupWorktreeMeta(deps.store.getProjectHostSetups?.() ?? [], repo),
displayName: resolvedFolderDisplayName ?? request.name,
...(displayNameRequest.kind === 'user' && resolvedFolderDisplayName
? { displayNameIsPinned: true }
: {}),
lastActivityAt: now,
createdAt: now,
orcaCreatedAt: now,
orcaCreationSource: 'runtime',
orcaCreationWorkspaceLayout: {
path: settings.workspaceDir,
nestWorkspaces: settings.nestWorkspaces
},
...(request.automationProvenance ? { automationProvenance: request.automationProvenance } : {}),
...(request.cliProvenance ? { cliProvenance: request.cliProvenance } : {}),
creatorProvenance: request.creatorProvenance ?? { kind: 'host' },
...(request.linkedIssue !== undefined ? { linkedIssue: request.linkedIssue } : {}),
...(request.linkedPR !== undefined ? { linkedPR: request.linkedPR } : {}),
...(request.linkedLinearIssue !== undefined
? { linkedLinearIssue: request.linkedLinearIssue }
: {}),
...(request.linkedLinearIssueWorkspaceId !== undefined
? { linkedLinearIssueWorkspaceId: request.linkedLinearIssueWorkspaceId }
: {}),
...(request.linkedLinearIssueOrganizationUrlKey !== undefined
? { linkedLinearIssueOrganizationUrlKey: request.linkedLinearIssueOrganizationUrlKey }
: {}),
...(request.linkedGitLabIssue !== undefined
? { linkedGitLabIssue: request.linkedGitLabIssue }
: {}),
...(request.linkedGitLabMR !== undefined ? { linkedGitLabMR: request.linkedGitLabMR } : {}),
...(request.linkedBitbucketPR !== undefined
? { linkedBitbucketPR: request.linkedBitbucketPR }
: {}),
...(request.linkedAzureDevOpsPR !== undefined
? { linkedAzureDevOpsPR: request.linkedAzureDevOpsPR }
: {}),
...(request.linkedGiteaPR !== undefined ? { linkedGiteaPR: request.linkedGiteaPR } : {}),
...(request.linkedWorkItem !== undefined ? { linkedWorkItem: request.linkedWorkItem } : {}),
...(request.linkedItems !== undefined ? { linkedItems: request.linkedItems } : {}),
...(request.linkedTaskSourceContext !== undefined
? { linkedTaskSourceContext: request.linkedTaskSourceContext }
: {}),
...(args.createdWithAgent ? { createdWithAgent: args.createdWithAgent } : {}),
...(request.comment !== undefined ? { comment: request.comment } : {}),
...(request.manualOrder !== undefined ? { manualOrder: request.manualOrder } : {}),
...(request.workspaceStatus !== undefined ? { workspaceStatus: request.workspaceStatus } : {})
})
args.onWorkspaceCreated?.(worktreeId)
const worktree = mergeRuntimeFolderWorkspace(repo, worktreeId, meta)
deps.invalidateResolvedWorktrees()
deps.notifyWorktreesChanged(repo.id)
deps.emitCreated({
kind: 'created',
worktreeId: worktree.id,
path: worktree.path,
branch: worktree.branch
})
const shouldActivate = request.activate === true || request.runHooks === true
let warning: string | undefined
let didSpawnStartup = false
let startupTerminal: CreateWorktreeResult['startupTerminal']
// Switch point: main starts the agent even with no terminal service, so its pane shows why not.
if (args.startup && (deps.ptySpawnAvailable || ORCA_LAYOUT_OWNER_IS_MAIN)) {
try {
const launchRuntime = args.startupLaunchRuntime
if (launchRuntime instanceof Error && !ORCA_LAYOUT_OWNER_IS_MAIN) {
throw launchRuntime
}
const terminal = await deps.createTerminal(`id:${worktree.id}`, {
command: args.startup.command,
...(request.startupCwd ? { cwd: request.startupCwd } : {}),
...paneIdentity(request.startupPaneKey),
env: args.startup.env,
...(args.startup.launchConfig ? { launchConfig: args.startup.launchConfig } : {}),
...(args.createdWithAgent ? { launchAgent: args.createdWithAgent } : {}),
...(launchRuntime instanceof Error
? { startFailure: launchRuntime }
: launchRuntime
? { launchRuntime }
: {}),
...(args.startup.viewMode ? { viewMode: args.startup.viewMode } : {}),
startupCommandDelivery: args.startup.startupCommandDelivery,
telemetry: args.startup.telemetry,
// Switch point: a start main could not make stays in its pane, showing why.
...(ORCA_LAYOUT_OWNER_IS_MAIN ? { keepPaneOnStartFailure: true as const } : {}),
...(shouldActivate ? {} : { surfaceOwner: false })
})
if (args.draftPaste) {
deps.pasteDraft(terminal.handle, args.draftPaste)
}
if (args.startupFollowup) {
deps.sendFollowup(terminal.handle, args.startupFollowup)
}
didSpawnStartup = true
startupTerminal = {
spawned: true,
handle: terminal.handle,
...(terminal.tabId ? { tabId: terminal.tabId } : {}),
...(terminal.paneKey ? { paneKey: terminal.paneKey } : {}),
...(terminal.ptyId ? { ptyId: terminal.ptyId } : {}),
surface: 'background'
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
warning = `Failed to create the startup terminal for ${worktree.path}: ${message}`
console.warn(`[worktree-create] ${warning}`)
}
}
if (shouldActivate) {
deps.activate(
repo.id,
worktree.id,
undefined,
// Switch point: a start main could not make stays main's to show, never the window's.
ORCA_LAYOUT_OWNER_IS_MAIN
? undefined
: args.startup && !didSpawnStartup
? args.startup
: undefined,
...(args.promisedAgentChat && !args.startup ? ([true] as const) : [])
)
}
if (
(!shouldActivate || deps.provisionInBackground?.() === true) &&
deps.ptySpawnAvailable &&
shouldSeedWorktreePrimaryTerminal({
// Switch point: a failed agent start keeps its pane, so no shell is seeded beside it.
hasPrimarySurface: didSpawnStartup || (ORCA_LAYOUT_OWNER_IS_MAIN && Boolean(args.startup)),
// Background attribution-only creates have historically omitted the shell.
callerProvidesSurface: args.promisedAgentChat === true || args.createdWithAgent !== undefined
})
) {
try {
await deps.createTerminal(`id:${worktree.id}`, { surfaceOwner: false })
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
const initialWarning = `Failed to create the initial terminal for ${worktree.path}: ${message}`
warning = warning
? `${warning} Also failed to create the initial terminal for ${worktree.path}: ${message}`
: initialWarning
console.warn(`[worktree-create] ${warning}`)
}
}
return {
worktree: {
...worktree,
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null,
git: {
path: worktree.path,
head: worktree.head,
branch: worktree.branch,
isBare: worktree.isBare,
isMainWorktree: worktree.isMainWorktree
}
},
...(startupTerminal ? { startupTerminal } : {}),
...(warning ? { warning } : {})
}
}