mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 00:02:54 +00:00
* feat(agent-launch): let a caller reserve the pane its terminal launch creates * fix(agent-launch): refuse a launch whose reserved pane is already live * fix(agent-launch): refuse a live reserved pane before it is revealed The live-pane refusal used to fire in the executor, after createTerminal had already issued a handle, published the mobile snapshot and revealed the tab. The reveal re-registered a fresh launch config over the running agent's. agent.launch now passes requireFreshPane with a reserved pane, and createTerminal throws AgentLaunchPaneAlreadyLiveError as soon as spawn reports it attached to a live pane. That is before any handle, snapshot or reveal. The spawn reattach itself is the one terminal.create already uses, so the live PTY is never killed, and the stable-pane create claim is still released in finally. The isReattach plumbing added to the launch factory for the old check is gone. A replay-safe launch refused this way on an existing workspace now records a failed ledger row, the same way a name collision does. Before, the row stayed claimed, so every retry got agent_session_operation_unknown. agent.launchReplay passes the code through. On create-worktree the workspace already exists when the terminal is refused, so the row stays unknown. The code is added to the runtime passthrough list so callers can branch on it. The pane key is now in the replay fingerprint, deliberately. It is not placement: group, anchor and focus still stay out of the request and out of the ledger. It is identity. It is written into the pane's PTY environment and names the tab the caller has placed. A retry that reserved a different pane is therefore a different request. Replaying the first answer would return a key the new reservation can never find. This matches terminal.createAgentSession, which also fingerprints its tab and leaf ids. The key is only folded in when present, so every existing digest is unchanged, and a test pins that. The wire schema now refuses a tab id the runtime would not adopt as sent: one with surrounding whitespace, which the runtime trims, and one longer than 512 characters, which the spawn reservation does not key on. It reuses the tab-id schema that Placement uses. * test(agent-launch): pin that a refused live pane issues no handle The refusal test named handle issuance but only asserted the reveal, so a throw moved to just before the reveal would still pass. Assert no terminal is registered, with the attach test as the positive control.
208 lines
8.4 KiB
TypeScript
208 lines
8.4 KiB
TypeScript
import { paneIdentity } from './runtime-terminal-pane-identity'
|
|
import { randomUUID } from 'node:crypto'
|
|
import { getProjectHostSetupWorktreeMeta } from '../../shared/project-host-setup-lookup'
|
|
import { resolveWorktreeCreateDisplayNameRequest } from '../ipc/worktree-logic'
|
|
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 {
|
|
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'
|
|
|
|
type RuntimeFolderWorktreeCreateDeps = {
|
|
store: RuntimeStore
|
|
ptySpawnAvailable: boolean
|
|
createTerminal: (
|
|
selector: string,
|
|
options: TerminalCreateOptions
|
|
) => Promise<RuntimeTerminalCreate>
|
|
markTrusted: (agent: TuiAgent, path: string) => Promise<void>
|
|
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
|
|
) => void
|
|
}
|
|
|
|
export async function createRuntimeFolderWorktree(args: {
|
|
request: RuntimeManagedWorktreeCreateArgs
|
|
repo: Repo
|
|
startup?: WorktreeStartupLaunch
|
|
startupFollowup?: WorktreeStartupFollowup
|
|
createdWithAgent?: TuiAgent
|
|
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(worktreeId, {
|
|
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.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 } : {})
|
|
})
|
|
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']
|
|
if (args.startup && deps.ptySpawnAvailable) {
|
|
try {
|
|
const trustAgent = args.draftPaste?.agent ?? args.createdWithAgent
|
|
if (trustAgent) {
|
|
await deps.markTrusted(trustAgent, worktree.path)
|
|
}
|
|
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 } : {}),
|
|
...(args.startup.viewMode ? { viewMode: args.startup.viewMode } : {}),
|
|
startupCommandDelivery: args.startup.startupCommandDelivery,
|
|
telemetry: args.startup.telemetry,
|
|
...(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,
|
|
args.startup && !didSpawnStartup ? args.startup : undefined
|
|
)
|
|
} else if (deps.ptySpawnAvailable && !didSpawnStartup && !args.createdWithAgent) {
|
|
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 } : {})
|
|
}
|
|
}
|