Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/scripts/vitest-sqlite-runtime-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/runtime/rpc/methods/agent-launch-prestart-failure.test.ts',
'src/main/runtime/rpc/methods/agent-launch-replay.test.ts',
'src/main/runtime/rpc/methods/agent-launch-session-reservation.test.ts',
'src/main/runtime/rpc/methods/agent-launch-workspace-activation.test.ts',
'src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts',
'src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts',
'src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export class OrcaRuntimeWithCreateManagedRemoteWorktree extends OrcaRuntimeWithC
setup,
startup,
defaultTabs,
args.navigation
args.navigation,
args.launchActivation
),
invalidateResolvedWorktrees: () => this.invalidateResolvedWorktreeCache(),
invalidateWorktreeScan: (repoId) => this.invalidateWorktreeScanCacheForRepo(repoId),
Expand Down
35 changes: 16 additions & 19 deletions src/main/runtime/orca-runtime-create-managed-worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { ExecutionHostNotDispatchableError } from '../providers/execution-host-p
import { createRuntimeFolderWorktree } from './runtime-folder-worktree-create'
import { createRuntimeLocalManagedWorktree } from './runtime-local-worktree-create'
import type { PreparationRearmHolder } from '../worktree-create-preparation'
import { prepareRuntimeLocalWorktreeSetup } from './runtime-local-worktree-setup'
import {
buildRuntimeLocalWorktreeSetupReceipt,
prepareRuntimeLocalWorktreeSetup
} from './runtime-local-worktree-setup'
import { invalidateAuthorizedRootsCacheForRepo } from '../ipc/filesystem-auth'
import { startRuntimeLocalWorktreeTerminals } from './runtime-local-worktree-terminal-startup'
import { trackRuntimeWorkspaceCreate } from '../workspace-create-telemetry'
Expand Down Expand Up @@ -111,7 +114,8 @@ export class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWithGetWork
setup,
startup,
undefined,
args.navigation
args.navigation,
args.launchActivation
)
}
})
Expand Down Expand Up @@ -255,7 +259,8 @@ export class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWithGetWork
activationSetup,
startup,
activationDefaultTabs,
args.navigation
args.navigation,
args.launchActivation
)
}
})
Expand All @@ -279,23 +284,15 @@ export class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWithGetWork
...(returnedSetup ? { setup: returnedSetup } : {}),
...(args.awaitTerminalProvisioning
? {
setupReceipt: {
requested: effectiveDecision,
setupReceipt: buildRuntimeLocalWorktreeSetupReceipt({
effectiveDecision,
hookFound,
startupPolicy: setup?.waitForAgentStartup
? ('wait-for-setup' as const)
: ('start-immediately' as const),
state: !hookFound
? ('not_configured' as const)
: effectiveDecision === 'skip' || !shouldRunSetup
? ('skipped' as const)
: // Why: the in-process hook is already executing, so reporting
// spawn_failed would strand callers that retry on it.
didSpawnSetup || didStartInProcessSetupHook
? ('running' as const)
: ('spawn_failed' as const),
...(setupTerminalHandle ? { terminalHandle: setupTerminalHandle } : {})
}
setup,
shouldRunSetup,
didSpawnSetup,
didStartInProcessSetupHook,
setupTerminalHandle
})
}
: {}),
...(defaultTabs ? { defaultTabs } : {}),
Expand Down
39 changes: 31 additions & 8 deletions src/main/runtime/orca-runtime-notify-ssh-state-changed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import type { CreateWorktreeResult } from '../../shared/worktree/create-types'
import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types'
import type { RuntimeNavigationTarget } from '../../shared/runtime-navigation'
import { navigationTargetsClients, navigationTargetsHost } from '../../shared/runtime-navigation'
import { toRuntimeActivateWorktreeEvent } from '../../shared/runtime-client-events'
import {
toRuntimeActivateWorktreeEvent,
type RuntimeActivatedLaunch
} from '../../shared/runtime-client-events'
import type { AgentBrowserBridge } from '../browser/agent-browser-bridge'
import type { BrowserBackend } from '../browser/browser-backend'
import type { EmulatorBridge } from '../emulator/emulator-bridge'
Expand Down Expand Up @@ -163,11 +166,12 @@ export class OrcaRuntimeWithNotifySshStateChanged extends OrcaRuntimeWithGetStat
setup?: CreateWorktreeResult['setup'],
startup?: WorktreeStartupLaunch,
defaultTabs?: CreateWorktreeResult['defaultTabs'],
navigationTarget?: RuntimeNavigationTarget
navigationTarget?: RuntimeNavigationTarget,
launch?: RuntimeActivatedLaunch
): void {
const navigation = navigationTarget ?? 'host'
if (navigationTargetsHost(navigation)) {
this.notifyHostActivateWorktree(repoId, worktreeId, setup, startup, defaultTabs)
this.notifyHostActivateWorktree(repoId, worktreeId, setup, startup, defaultTabs, launch)
}
if (navigationTargetsClients(navigation)) {
this.notifyClientsActivateWorktree(
Expand All @@ -176,7 +180,8 @@ export class OrcaRuntimeWithNotifySshStateChanged extends OrcaRuntimeWithGetStat
setup,
startup,
defaultTabs,
navigation
navigation,
launch
)
}
}
Expand All @@ -186,9 +191,18 @@ export class OrcaRuntimeWithNotifySshStateChanged extends OrcaRuntimeWithGetStat
worktreeId: string,
setup?: CreateWorktreeResult['setup'],
startup?: WorktreeStartupLaunch,
defaultTabs?: CreateWorktreeResult['defaultTabs']
defaultTabs?: CreateWorktreeResult['defaultTabs'],
launch?: RuntimeActivatedLaunch
): void {
this.notifier?.activateWorktree(repoId, worktreeId, setup, startup, defaultTabs)
// Every other activation keeps its call exactly as before.
this.notifier?.activateWorktree(
repoId,
worktreeId,
setup,
startup,
defaultTabs,
...(launch ? [launch] : [])
)
}

protected notifyClientsActivateWorktree(
Expand All @@ -197,10 +211,19 @@ export class OrcaRuntimeWithNotifySshStateChanged extends OrcaRuntimeWithGetStat
setup?: CreateWorktreeResult['setup'],
startup?: WorktreeStartupLaunch,
defaultTabs?: CreateWorktreeResult['defaultTabs'],
navigation: RuntimeNavigationTarget = 'clients'
navigation: RuntimeNavigationTarget = 'clients',
launch?: RuntimeActivatedLaunch
): void {
this.emitClientEvent(
toRuntimeActivateWorktreeEvent(repoId, worktreeId, setup, startup, defaultTabs, navigation)
toRuntimeActivateWorktreeEvent(
repoId,
worktreeId,
setup,
startup,
defaultTabs,
navigation,
launch
)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,54 @@ describe('OrcaRuntimeService', () => {
)
})

it('names the desktop launch that created the worktree on its activation', async () => {
const runtime = new OrcaRuntimeService(store)
const activateWorktree = vi.fn()
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree,
createTerminal: vi.fn(),
splitTerminal: vi.fn(),
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
sleepWorktree: vi.fn(),
terminalFitOverrideChanged: vi.fn(),
terminalDriverChanged: vi.fn()
})
runtime.attachWindow(1)

computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-launch')
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-launch')
vi.mocked(getEffectiveHooks).mockReturnValue(null)
vi.mocked(listWorktrees).mockResolvedValueOnce([
{
path: '/tmp/workspaces/runtime-launch',
head: 'def',
branch: 'runtime-launch',
isBare: false,
isMainWorktree: false
}
])

await runtime.createManagedWorktree({
repoSelector: 'id:repo-1',
name: 'runtime-launch',
activate: true,
launchActivation: { operationId: 'op-1' }
})

expect(activateWorktree).toHaveBeenCalledWith(
'repo-1',
expect.any(String),
undefined,
undefined,
undefined,
{ operationId: 'op-1' }
)
})

it('stamps createdAt alongside lastActivityAt so CLI-created worktrees get the Recent-sort grace window', async () => {
// Why: without createdAt, ambient PTY bumps in other worktrees can push the new one below them in Recent sort.
const runtime = new OrcaRuntimeService(store)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* A desktop `agent.launch` create names itself on the activation that opens its workspace, so the
* window that asked binds the workspace to that launch. A phone's or the CLI's create never does:
* a paired device's launch leaves the desktop window alone, and the CLI's window holds no launch.
*/

import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { AgentSessionRecordStore } from '../../agent-session-record-store'
import { openTestAgentSessionRecordStore } from '../../agent-session-record-store-test-harness'
import type { RpcContext } from '../core'
import { DESKTOP_RPC_CALLER } from '../rpc-caller-identity'
import {
CAPABLE_CLIENT,
methodNamed,
rpcContext,
runtimeStub,
setAgentLaunchRecordStore
} from './agent-launch.test-fixture'

vi.mock('./structured-agent-session-create', () => ({
createStructuredAgentSessionForWorktree: async () => ({
ok: true,
value: { sessionId: 'sess-1' }
})
}))

const { AGENT_LAUNCH_METHODS } = await import('./agent-launch')
const AGENT_LAUNCH_REPLAY = methodNamed(AGENT_LAUNCH_METHODS, 'agent.launchReplay')

// The ledger admits against `Date.now()`, so the id must be dated now.
const OPERATION_ID = `${Date.now()}-000000000000000000000000000000a7`

let directory: string
let store: AgentSessionRecordStore

beforeEach(async () => {
directory = await mkdtemp(join(tmpdir(), 'orca-agent-launch-activation-'))
store = await openTestAgentSessionRecordStore(directory)
setAgentLaunchRecordStore(store)
})

afterEach(async () => {
setAgentLaunchRecordStore(null)
await rm(directory, { recursive: true, force: true })
})

async function createArgsFor(context: Partial<RpcContext>, activate: boolean) {
const runtime = runtimeStub()
await AGENT_LAUNCH_REPLAY.handler(
AGENT_LAUNCH_REPLAY.params.parse({
agent: 'claude',
target: { kind: 'create-worktree', create: { repo: 'id:repo-1', name: 'task', activate } },
operationId: OPERATION_ID
}),
rpcContext(runtime, context)
)
const [args] = runtime.createManagedWorktree.mock.calls[0] ?? []
return args
}

describe('the activation of a workspace an agent.launch creates', () => {
it('names the desktop’s own launch', async () => {
const args = await createArgsFor({ caller: DESKTOP_RPC_CALLER }, true)
expect(args?.launchActivation).toEqual({ operationId: OPERATION_ID })
})

it.each([
['a paired phone', CAPABLE_CLIENT],
['the CLI', {}]
] as const)('never names %s’s launch', async (_who, context) => {
const args = await createArgsFor(context, true)
expect(args).not.toHaveProperty('launchActivation')
})

it('names nothing for a create that does not open its workspace', async () => {
const args = await createArgsFor({ caller: DESKTOP_RPC_CALLER }, false)
expect(args).not.toHaveProperty('launchActivation')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ const STRUCTURED_SETUP_WAIT_TIMEOUT_MS = 60_000

export function agentLaunchWorkspaceFactory(
context: RpcContext,
agent: TuiAgent
agent: TuiAgent,
/** The launch's own id, absent for an unrecorded launch. */
operationId?: string
): AgentLaunchWorkspaceFactory {
return {
createWorktree: async ({
Expand Down Expand Up @@ -96,6 +98,10 @@ export function agentLaunchWorkspaceFactory(
// The launch owns the agent whichever surface it settles on, so the workspace records
// it even when no startup terminal was created for it.
createdWithAgent: agent,
// Only the desktop's own launch opens the workspace in its window as that launch's.
...(operationId && context.caller?.kind === 'desktop' && params.activate === true
? { launchActivation: { operationId } }
: {}),
// Structured sessions have no startup command to sequence behind setup. Provision the
// setup terminal synchronously and attach a completion token so the launch can wait
// before creating the chat surface.
Expand Down
4 changes: 3 additions & 1 deletion src/main/runtime/rpc/methods/agent-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ function callerRendersLaunchedChat(

/** What a launch admitted under an operation id carries into its execution. */
type ReplaySafeLaunch = {
operationId: string
attachOperationId: string
callerKey: string
terminalSpawn: TerminalSpawnDispatch
Expand Down Expand Up @@ -125,7 +126,7 @@ async function runAgentLaunch(
replaySafe?.terminalSpawn,
view.early
),
workspaces: agentLaunchWorkspaceFactory(context, intent.agent),
workspaces: agentLaunchWorkspaceFactory(context, intent.agent, replaySafe?.operationId),
...(callerRendersLaunchedChat(context, intent.agent) ? {} : { callerRendersStructured: false }),
// The tab is shown as it is published, not after a prompt that can take a minute to land.
onSurfacePublished: (surface) => {
Expand Down Expand Up @@ -247,6 +248,7 @@ async function executeAdmittedAgentLaunch(
let result: AgentLaunchResult
try {
result = await runAgentLaunch(intent, context, view, {
operationId: params.operationId,
attachOperationId: admission.attachOperationId,
callerKey: admission.callerKey,
terminalSpawn,
Expand Down
Loading
Loading