mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 01:27:35 +00:00
refactor(runtime): main alone starts setup and default tabs for an activated create (#28153)
* refactor(runtime): main alone starts setup and default tabs for an activated create An activated CLI or paired create with a window open handed setup and default tabs to the host window, and with navigation 'all' to paired clients too, so both could run setup. Main now provisions them itself. Only when main tried to start setup and failed does it hand setup to the host window, which retries it where its output or spawn error is visible. * fix(runtime): hand the window the whole create when the agent fails to start Main provisioned setup and default tabs after a failed agent start and sent the window only the startup. The window found tabs, seeded nothing, and the agent never launched. A failed agent start now provisions nothing and hands startup, setup and default tabs to the host window, as before this PR.
This commit is contained in:
@@ -87,14 +87,7 @@ export class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntimeWithListM
|
||||
this.notifyHostActivateWorktree(repo.id, worktree.id)
|
||||
}
|
||||
if (targetsClients) {
|
||||
this.notifyClientsActivateWorktree(
|
||||
repo.id,
|
||||
worktree.id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
navigation
|
||||
)
|
||||
this.notifyClientsActivateWorktree(repo.id, worktree.id, undefined, navigation)
|
||||
}
|
||||
}
|
||||
if (!targetsHost) {
|
||||
|
||||
@@ -216,13 +216,13 @@ export class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWithGetWork
|
||||
pasteDraft: (handle, draft) => this.pasteStartupDraftWhenReady(handle, draft),
|
||||
sendFollowup: (handle, followup) => this.sendStartupFollowupWhenReady(handle, followup),
|
||||
provision: (options) => this.provisionManagedWorktreeTerminals(options, worktree),
|
||||
activate: (repoId, worktreeId, activationSetup, startup, activationDefaultTabs) =>
|
||||
activate: (repoId, worktreeId, startup, failedSetup, failedDefaultTabs) =>
|
||||
this.notifyActivateWorktree(
|
||||
repoId,
|
||||
worktreeId,
|
||||
activationSetup,
|
||||
failedSetup,
|
||||
startup,
|
||||
activationDefaultTabs,
|
||||
failedDefaultTabs,
|
||||
args.navigation
|
||||
)
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ export class OrcaRuntimeWithNotifySshStateChanged extends OrcaRuntimeWithGetStat
|
||||
this.emitClientEvent({ type: 'reposChanged' })
|
||||
}
|
||||
|
||||
/** `setup` and `defaultTabs` go to the host window only: main tried and failed to start them. */
|
||||
protected notifyActivateWorktree(
|
||||
repoId: string,
|
||||
worktreeId: string,
|
||||
@@ -170,14 +171,7 @@ export class OrcaRuntimeWithNotifySshStateChanged extends OrcaRuntimeWithGetStat
|
||||
this.notifyHostActivateWorktree(repoId, worktreeId, setup, startup, defaultTabs)
|
||||
}
|
||||
if (navigationTargetsClients(navigation)) {
|
||||
this.notifyClientsActivateWorktree(
|
||||
repoId,
|
||||
worktreeId,
|
||||
setup,
|
||||
startup,
|
||||
defaultTabs,
|
||||
navigation
|
||||
)
|
||||
this.notifyClientsActivateWorktree(repoId, worktreeId, startup, navigation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,13 +188,11 @@ export class OrcaRuntimeWithNotifySshStateChanged extends OrcaRuntimeWithGetStat
|
||||
protected notifyClientsActivateWorktree(
|
||||
repoId: string,
|
||||
worktreeId: string,
|
||||
setup?: CreateWorktreeResult['setup'],
|
||||
startup?: WorktreeStartupLaunch,
|
||||
defaultTabs?: CreateWorktreeResult['defaultTabs'],
|
||||
navigation: RuntimeNavigationTarget = 'clients'
|
||||
): void {
|
||||
this.emitClientEvent(
|
||||
toRuntimeActivateWorktreeEvent(repoId, worktreeId, setup, startup, defaultTabs, navigation)
|
||||
toRuntimeActivateWorktreeEvent(repoId, worktreeId, undefined, startup, undefined, navigation)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RuntimeClientEvent } from '../../../shared/runtime-client-events'
|
||||
import { getDefaultWorkspaceSession } from '../../../shared/constants'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
@@ -377,7 +378,7 @@ describe('OrcaRuntimeService', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('passes the wrapped setup command to activation when startup spawned but setup did not', async () => {
|
||||
it('hands only the host window a setup main tried and failed to start', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const spawn = vi
|
||||
.fn()
|
||||
@@ -405,6 +406,8 @@ describe('OrcaRuntimeService', () => {
|
||||
terminalDriverChanged: vi.fn()
|
||||
})
|
||||
runtime.attachWindow(1)
|
||||
const events: RuntimeClientEvent[] = []
|
||||
const unsubscribe = runtime.onClientEvent((event) => events.push(event))
|
||||
|
||||
computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-startup-setup-retry')
|
||||
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-startup-setup-retry')
|
||||
@@ -438,8 +441,10 @@ describe('OrcaRuntimeService', () => {
|
||||
name: 'runtime-startup-setup-retry',
|
||||
setupDecision: 'run',
|
||||
activate: true,
|
||||
navigation: 'all',
|
||||
startup: { command: 'claude' }
|
||||
})
|
||||
unsubscribe()
|
||||
|
||||
expect(spawn).toHaveBeenCalledTimes(2)
|
||||
expect(activateWorktree).toHaveBeenCalledWith(
|
||||
@@ -452,6 +457,11 @@ describe('OrcaRuntimeService', () => {
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
expect(activateWorktree).toHaveBeenCalledOnce()
|
||||
// The window retries it where the user sees its output or its spawn error; clients never run it.
|
||||
expect(
|
||||
events.filter((event) => event.type === 'activateWorktree' && event.setup !== undefined)
|
||||
).toEqual([])
|
||||
const activationSetup = activateWorktree.mock.calls[0]?.[2] as { command?: string } | undefined
|
||||
expect(activationSetup?.command).toContain('printf')
|
||||
})
|
||||
|
||||
@@ -95,16 +95,17 @@ describe('OrcaRuntimeService', () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
// Main never tried to start setup (no terminal host), so the window is handed nothing.
|
||||
expect(activateWorktree).toHaveBeenCalledWith(
|
||||
'repo-1',
|
||||
expect.any(String),
|
||||
result.setup,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('passes setup payloads through when explicitly activating CLI-created worktrees', async () => {
|
||||
it('does not hand the window a setup main never tried to start', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const activateWorktree = vi.fn()
|
||||
runtime.setNotifier({
|
||||
@@ -153,10 +154,12 @@ describe('OrcaRuntimeService', () => {
|
||||
activate: true
|
||||
})
|
||||
|
||||
expect(result.setup).toMatchObject({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh' })
|
||||
// Main never tried to start setup (no terminal host), so the window is handed nothing.
|
||||
expect(activateWorktree).toHaveBeenCalledWith(
|
||||
'repo-1',
|
||||
expect.any(String),
|
||||
result.setup,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
@@ -233,10 +236,11 @@ describe('OrcaRuntimeService', () => {
|
||||
runnerScriptPath: 'C:\\repo\\.git\\orca\\setup-runner.sh',
|
||||
shell: { family: 'posix' }
|
||||
})
|
||||
// Main never tried to start setup (no terminal host), so the window is handed nothing.
|
||||
expect(activateWorktree).toHaveBeenCalledWith(
|
||||
'repo-1',
|
||||
expect.any(String),
|
||||
result.setup,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { RuntimeClientEvent } from '../../../shared/runtime-client-events'
|
||||
import type { WorktreeStartupLaunch } from '../../../shared/worktree/launch-types'
|
||||
import {
|
||||
OrcaRuntimeService,
|
||||
computeWorktreePathMock,
|
||||
createSetupRunnerScript,
|
||||
ensurePathWithinWorkspaceMock,
|
||||
getEffectiveHooks,
|
||||
listWorktrees,
|
||||
setRuntimeDesktopSurface,
|
||||
shouldRunSetupForCreate
|
||||
} from '../orca-runtime-test-mocks.spec'
|
||||
import { store } from '../orca-runtime-test-fixtures.spec'
|
||||
|
||||
const WORKTREE_PATH = '/tmp/workspaces/runtime-setup-all'
|
||||
const RUNNER = '/tmp/repo/.git/orca/setup-runner.sh'
|
||||
|
||||
/** An activated create with a live window, navigating the window and paired clients. */
|
||||
async function createActivated(options: {
|
||||
startup?: WorktreeStartupLaunch
|
||||
failAgentSpawn?: boolean
|
||||
}): Promise<{
|
||||
activateWorktree: ReturnType<typeof vi.fn>
|
||||
events: RuntimeClientEvent[]
|
||||
spawnedCommands: (string | undefined)[]
|
||||
}> {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const activateWorktree = vi.fn()
|
||||
let spawned = 0
|
||||
const spawn = vi.fn(async (args: { command?: string }) => {
|
||||
if (options.failAgentSpawn && args.command === options.startup?.command) {
|
||||
throw new Error('agent_session_exited_during_start')
|
||||
}
|
||||
return { id: `pty-${++spawned}` }
|
||||
})
|
||||
runtime.setPtyController({
|
||||
spawn,
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
runtime.setNotifier({
|
||||
worktreesChanged: vi.fn(),
|
||||
reposChanged: vi.fn(),
|
||||
activateWorktree,
|
||||
createTerminal: vi.fn(),
|
||||
revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-revealed' }),
|
||||
splitTerminal: vi.fn(),
|
||||
renameTerminal: vi.fn(),
|
||||
focusTerminal: vi.fn(),
|
||||
closeTerminal: vi.fn(),
|
||||
sleepWorktree: vi.fn(),
|
||||
terminalFitOverrideChanged: vi.fn(),
|
||||
terminalDriverChanged: vi.fn()
|
||||
})
|
||||
// A live window, so main hands work to it instead of provisioning in the background.
|
||||
const liveWindow = { isDestroyed: () => false, webContents: { isDestroyed: () => false } }
|
||||
setRuntimeDesktopSurface({
|
||||
showNotification: () => false,
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the create path reads only the window's liveness.
|
||||
findWindowById: () => liveWindow as never,
|
||||
onIpc: () => {},
|
||||
removeIpcListener: () => {}
|
||||
})
|
||||
runtime.attachWindow(1)
|
||||
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
|
||||
const events: RuntimeClientEvent[] = []
|
||||
const unsubscribe = runtime.onClientEvent((event) => events.push(event))
|
||||
|
||||
computeWorktreePathMock.mockReturnValue(WORKTREE_PATH)
|
||||
ensurePathWithinWorkspaceMock.mockReturnValue(WORKTREE_PATH)
|
||||
vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } })
|
||||
vi.mocked(shouldRunSetupForCreate).mockReturnValue(true)
|
||||
vi.mocked(createSetupRunnerScript).mockReturnValue({
|
||||
runnerScriptPath: RUNNER,
|
||||
envVars: { ORCA_ROOT_PATH: '/tmp/repo', ORCA_WORKTREE_PATH: WORKTREE_PATH }
|
||||
})
|
||||
vi.mocked(listWorktrees).mockResolvedValue([
|
||||
{
|
||||
path: WORKTREE_PATH,
|
||||
head: 'def',
|
||||
branch: 'runtime-setup-all',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
])
|
||||
|
||||
try {
|
||||
await runtime.createManagedWorktree({
|
||||
repoSelector: 'id:repo-1',
|
||||
name: 'runtime-setup-all',
|
||||
setupDecision: 'run',
|
||||
activate: true,
|
||||
navigation: 'all',
|
||||
...(options.startup ? { startup: options.startup } : {})
|
||||
})
|
||||
} finally {
|
||||
unsubscribe()
|
||||
setRuntimeDesktopSurface(null)
|
||||
}
|
||||
return {
|
||||
activateWorktree,
|
||||
events,
|
||||
spawnedCommands: spawn.mock.calls.map(([args]) => args.command)
|
||||
}
|
||||
}
|
||||
|
||||
describe('OrcaRuntimeService setup creator', () => {
|
||||
it('creates the setup terminal once when an activated create navigates the window and clients', async () => {
|
||||
const { activateWorktree, events, spawnedCommands } = await createActivated({})
|
||||
|
||||
const hostHandoffs = activateWorktree.mock.calls.filter((call) => call[2] !== undefined)
|
||||
const clientHandoffs = events.filter(
|
||||
(event) => event.type === 'activateWorktree' && event.setup !== undefined
|
||||
)
|
||||
expect({
|
||||
hostHandoffs: hostHandoffs.length,
|
||||
clientHandoffs: clientHandoffs.length,
|
||||
setupSpawns: spawnedCommands.filter((command) => command?.includes(RUNNER)).length
|
||||
}).toEqual({ hostHandoffs: 0, clientHandoffs: 0, setupSpawns: 1 })
|
||||
})
|
||||
|
||||
it('hands the host window the whole create when the agent fails to start', async () => {
|
||||
const startup = { command: 'claude' }
|
||||
const { activateWorktree, events, spawnedCommands } = await createActivated({
|
||||
startup,
|
||||
failAgentSpawn: true
|
||||
})
|
||||
|
||||
// Main provisions nothing, so the window seeds the agent's tab and lays setup out around it.
|
||||
expect(spawnedCommands).toEqual(['claude'])
|
||||
expect(activateWorktree).toHaveBeenCalledOnce()
|
||||
expect(activateWorktree).toHaveBeenCalledWith(
|
||||
'repo-1',
|
||||
expect.any(String),
|
||||
expect.objectContaining({ runnerScriptPath: RUNNER }),
|
||||
expect.objectContaining({ command: 'claude' }),
|
||||
undefined
|
||||
)
|
||||
expect(
|
||||
events.filter((event) => event.type === 'activateWorktree' && event.setup !== undefined)
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -110,6 +110,7 @@ await import('./orca-runtime-tests/worktree-setup-and-startup.spec')
|
||||
await import('./orca-runtime-tests/worktree-setup-and-startup-part-02.spec')
|
||||
await import('./orca-runtime-tests/worktree-setup-and-startup-part-03.spec')
|
||||
await import('./orca-runtime-tests/worktree-setup-and-startup-part-04.spec')
|
||||
await import('./orca-runtime-tests/worktree-setup-single-creator.spec')
|
||||
await import('./orca-runtime-tests/gitlab-and-pr-bases.spec')
|
||||
await import('./orca-runtime-tests/gitlab-and-pr-bases-part-02.spec')
|
||||
await import('./orca-runtime-tests/gitlab-and-pr-bases-part-03.spec')
|
||||
|
||||
@@ -33,9 +33,10 @@ type Ports = {
|
||||
activate: (
|
||||
repoId: string,
|
||||
worktreeId: string,
|
||||
setup?: CreateWorktreeResult['setup'],
|
||||
startup?: WorktreeStartupLaunch,
|
||||
defaultTabs?: CreateWorktreeResult['defaultTabs']
|
||||
// Only work main tried to start and failed; the host window retries it where the user sees it.
|
||||
failedSetup?: CreateWorktreeResult['setup'],
|
||||
failedDefaultTabs?: CreateWorktreeResult['defaultTabs']
|
||||
) => void
|
||||
}
|
||||
|
||||
@@ -125,32 +126,40 @@ export async function startRuntimeLocalWorktreeTerminals(args: {
|
||||
|
||||
if (shouldActivate) {
|
||||
const provisionInBackground = ports.provisionInBackground?.() === true
|
||||
const runtimeWillProvision =
|
||||
(provisionInBackground && ports.canSpawn) ||
|
||||
(didSpawnStartup && Boolean(setup || defaultTabs))
|
||||
if (runtimeWillProvision) {
|
||||
const provisioned = await ports.provision({
|
||||
...provisionArgs(args, startupTerminalHandle, didSpawnStartup, wrappedSetupCommand),
|
||||
...(provisionInBackground ? { surfaceOwner: false as const } : {})
|
||||
})
|
||||
didSpawnSetup = provisioned.setupSpawned
|
||||
setupTerminalHandle = provisioned.setupTerminalHandle
|
||||
}
|
||||
const activationSetup = didSpawnSetup
|
||||
? undefined
|
||||
: setup
|
||||
? {
|
||||
// Why: a failed agent start hands the whole create to the window, which seeds the agent's tab
|
||||
// first and lays setup and default tabs around it; main provisioning first would leave it no tab.
|
||||
const agentStartFailed = Boolean(sequencedStartup && ports.canSpawn && !didSpawnStartup)
|
||||
if (agentStartFailed && !provisionInBackground) {
|
||||
ports.activate(repo.id, worktree.id, startup, setup, defaultTabs)
|
||||
} else {
|
||||
let failedSetup: CreateWorktreeResult['setup']
|
||||
// Why: main alone creates the setup and default tabs; the window draws them from the reveals.
|
||||
if (ports.canSpawn && (provisionInBackground || setup || defaultTabs)) {
|
||||
const provisioned = await ports.provision({
|
||||
...provisionArgs(
|
||||
args,
|
||||
startupTerminalHandle,
|
||||
didSpawnStartup,
|
||||
didSpawnStartup ? wrappedSetupCommand : undefined
|
||||
),
|
||||
...(provisionInBackground ? { surfaceOwner: false as const } : {})
|
||||
})
|
||||
didSpawnSetup = provisioned.setupSpawned
|
||||
setupTerminalHandle = provisioned.setupTerminalHandle
|
||||
if (setup && !didSpawnSetup) {
|
||||
failedSetup = {
|
||||
...setup,
|
||||
...(didSpawnStartup && wrappedSetupCommand ? { command: wrappedSetupCommand } : {})
|
||||
}
|
||||
: undefined
|
||||
ports.activate(
|
||||
repo.id,
|
||||
worktree.id,
|
||||
activationSetup,
|
||||
startup && !didSpawnStartup ? startup : undefined,
|
||||
runtimeWillProvision ? undefined : defaultTabs
|
||||
)
|
||||
}
|
||||
}
|
||||
ports.activate(
|
||||
repo.id,
|
||||
worktree.id,
|
||||
startup && !didSpawnStartup ? startup : undefined,
|
||||
failedSetup
|
||||
)
|
||||
}
|
||||
} else if (ports.canSpawn && (setup || defaultTabs || didSpawnStartup)) {
|
||||
const provisioning = ports.provision({
|
||||
...provisionArgs(args, startupTerminalHandle, didSpawnStartup, wrappedSetupCommand),
|
||||
|
||||
Reference in New Issue
Block a user