mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 19:47:27 +00:00
* 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>
243 lines
10 KiB
TypeScript
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 } : {})
|
|
}
|
|
}
|