fix(workspaces): seed shells only for blank selection (#19940)

* fix(workspaces): seed shells only for blank selection

* fix(workspaces): create runtime-owned launch surfaces

* fix(workspaces): report runtime surface failures

---------

Co-authored-by: Merge Sim <sim@local>
This commit is contained in:
Brennan Benson
2026-09-10 22:05:30 -07:00
committed by GitHub
co-authored by Merge Sim
parent d33354cfd2
commit 2ffac471bd
24 changed files with 701 additions and 95 deletions
@@ -172,7 +172,7 @@ export async function createRuntimeFolderWorktree(args: {
undefined,
args.startup && !didSpawnStartup ? args.startup : undefined
)
} else if (deps.ptySpawnAvailable && !didSpawnStartup) {
} else if (deps.ptySpawnAvailable && !didSpawnStartup && !args.createdWithAgent) {
try {
await deps.createTerminal(`id:${worktree.id}`, { surfaceOwner: false })
} catch (error) {
@@ -0,0 +1,74 @@
import { describe, expect, it, vi } from 'vitest'
import type { Repo } from '../../shared/repo-types'
import type { Worktree } from '../../shared/worktree/types'
import { startRuntimeLocalWorktreeTerminals } from './runtime-local-worktree-terminal-startup'
const repo: Repo = {
id: 'repo-1',
path: '/repo',
displayName: 'repo',
badgeColor: 'blue',
addedAt: 1
}
const worktree: Worktree = {
id: 'worktree-1',
repoId: repo.id,
path: '/worktree',
head: 'abc',
branch: 'feature',
isBare: false,
isMainWorktree: false,
displayName: 'feature',
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1
}
type StartupArgs = Parameters<typeof startRuntimeLocalWorktreeTerminals>[0]
function createPorts() {
const createTerminal = vi.fn<StartupArgs['ports']['createTerminal']>().mockResolvedValue({
handle: 'term-1',
worktreeId: worktree.id,
title: null
})
const ports: StartupArgs['ports'] = {
canSpawn: true,
markTrusted: vi.fn(),
createTerminal,
pasteDraft: vi.fn(),
sendFollowup: vi.fn(),
provision: vi.fn().mockResolvedValue({ setupSpawned: false, setupTerminalHandle: null }),
activate: vi.fn()
}
return { createTerminal, ports }
}
describe('startRuntimeLocalWorktreeTerminals default shell seeding', () => {
it.each([
['Blank Terminal', undefined, 1],
['an agent', 'codex' as const, 0]
])('seeds a background shell for %s selection only', async (_label, agent, expectedCalls) => {
const { createTerminal, ports } = createPorts()
await startRuntimeLocalWorktreeTerminals({
request: { repoSelector: `id:${repo.id}`, name: worktree.displayName },
repo,
worktree,
...(agent ? { createdWithAgent: agent } : {}),
ports
})
expect(createTerminal).toHaveBeenCalledTimes(expectedCalls)
if (expectedCalls > 0) {
expect(createTerminal).toHaveBeenCalledWith(`id:${worktree.id}`, { surfaceOwner: false })
}
})
})
@@ -163,7 +163,7 @@ export async function startRuntimeLocalWorktreeTerminals(args: {
didSpawnSetup = true
}
}
} else if (ports.canSpawn) {
} else if (ports.canSpawn && !args.createdWithAgent) {
try {
await ports.createTerminal(`id:${worktree.id}`, { surfaceOwner: false })
} catch (error) {
@@ -222,7 +222,7 @@ export async function createRuntimeRemoteManagedWorktree(
didSpawnSetup = true
}
}
} else if (!shouldActivate && deps.canSpawn()) {
} else if (!shouldActivate && deps.canSpawn() && !args.createdWithAgent) {
try {
await deps.createTerminal(`path:${result.worktree.path}`, { surfaceOwner: false })
} catch (err) {
@@ -116,6 +116,7 @@ describe('submitFolderWorkspaceCreate', () => {
})
expect(onOpenChange).toHaveBeenCalledWith(false)
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith('folder-workspace-1', {
agent: null,
runtimeEnvironmentId: null
})
expect(consoleError).toHaveBeenCalledWith(
@@ -532,6 +533,7 @@ describe('submitFolderWorkspaceCreate', () => {
linkedTask: linkedWorkItem
})
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith('folder-workspace-1', {
agent: null,
runtimeEnvironmentId: null
})
expect(mocks.ensureAgentStartupInTerminal).not.toHaveBeenCalled()
@@ -659,6 +661,7 @@ describe('submitFolderWorkspaceCreate', () => {
})
expect(onOpenChange).toHaveBeenCalledWith(false)
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith('folder-workspace-1', {
agent: null,
runtimeEnvironmentId: null
})
})
@@ -207,6 +207,7 @@ export async function submitFolderWorkspaceCreate({
onOpenChange(false)
try {
let activation = activateAndRevealFolderWorkspace(workspace.id, {
agent: quickAgent,
...(!structuredLaunch && startup ? { startup } : {}),
...(structuredLaunch ? { providesInitialSurface: true } : {}),
runtimeEnvironmentId
@@ -229,6 +230,7 @@ export async function submitFolderWorkspaceCreate({
connectionId: workspace.connectionId ?? projectGroup.connectionId
})
const fallbackActivation = activateAndRevealFolderWorkspace(workspace.id, {
agent: quickAgent,
...(startup ? { startup } : {}),
runtimeEnvironmentId
})
@@ -219,6 +219,7 @@ export function useFullCreationExecution(input: FullCreationExecutionInput) {
const initialActivation = activateAndRevealWorktree(worktree.id, {
sidebarRevealBehavior: 'auto',
agent: tuiAgent,
setup: result.setup,
defaultTabs: result.defaultTabs,
issueCommand,
@@ -229,6 +230,7 @@ export function useFullCreationExecution(input: FullCreationExecutionInput) {
const settlement = await settleFullCreationStructuredLaunch({
plan: launchPlan,
agent: tuiAgent,
worktreeId: worktree.id,
startup,
pendingFirstAgentMessageRename,
@@ -36,6 +36,7 @@ const plan = (overrides: Partial<AgentSessionLaunchVerdict> = {}) =>
const baseArgs = {
plan: plan(),
agent: 'codex' as const,
worktreeId: 'worktree-1',
startup: { command: 'codex' } as never,
pendingFirstAgentMessageRename: true,
@@ -94,6 +95,7 @@ describe('settleFullCreationStructuredLaunch', () => {
})
expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith('worktree-1', {
sidebarRevealBehavior: 'auto',
agent: 'codex',
createNewTerminalForStartup: true,
startup: baseArgs.startup
})
@@ -3,12 +3,14 @@ import type { WorktreeStartupPayload } from '@/lib/worktree-startup-payload'
import { activateAndRevealWorktree } from '@/lib/worktree-activation'
import type { StructuredAgentLaunchSettlement } from '@/lib/structured-agent-launch-settlement'
import { activateStructuredAgentSessionById } from '@/lib/structured-agent-session-tab-activation'
import type { TuiAgent } from '../../../../shared/tui-agent'
/** Full-create dialog: the structured launch plus what this flow did before structured chat
* existed. Returns null when the plan's route is not structured. */
export async function settleFullCreationStructuredLaunch(args: {
/** Planned before the worktree existed; `worktreeId` names the one that was created. */
plan: AgentSessionLaunchPlan
agent: TuiAgent
worktreeId: string
startup: WorktreeStartupPayload | undefined
pendingFirstAgentMessageRename: boolean
@@ -27,6 +29,7 @@ export async function settleFullCreationStructuredLaunch(args: {
}
const activation = activateAndRevealWorktree(args.worktreeId, {
sidebarRevealBehavior: 'auto',
agent: args.agent,
createNewTerminalForStartup: true,
...(args.startup ? { startup: args.startup } : {})
})
@@ -11,50 +11,114 @@ import {
endWebRuntimeWakeTerminalRespawn
} from '@/runtime/web-runtime-wake-terminal-respawn'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import {
draftViewModeProps,
resolveStartupLaunchDraftText,
type WorktreeStartupPayload
} from '@/lib/worktree-startup-payload'
import type { TuiAgent } from '../../../shared/tui-agent'
import { initialAgentTabViewModeProps } from '@/lib/native-chat-initial-view-mode'
import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability'
import { getConnectionId } from '@/lib/connection-context'
import { toast } from 'sonner'
export function ensureWebRuntimeWorktreeTerminalAfterWake(worktreeId: string): void {
const state = useAppStore.getState()
const worktree = state.getKnownWorktreeById(worktreeId)
if (!worktree) {
return
export function ensureWebRuntimeWorktreeTerminalAfterWake(
worktreeId: string,
opts?: {
runtimeEnvironmentId?: string | null
startup?: WorktreeStartupPayload
agent?: TuiAgent | null
activate?: boolean
}
const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(state, worktree.id)
): void {
const state = useAppStore.getState()
const runtimeEnvironmentId =
opts && 'runtimeEnvironmentId' in opts
? (opts.runtimeEnvironmentId ?? null)
: getRuntimeEnvironmentIdForWorktree(state, worktreeId)
if (!runtimeEnvironmentId || !isWebRuntimeSessionActive(runtimeEnvironmentId)) {
return
}
const tabs = state.tabsByWorktree[worktreeId] ?? []
const hasLivePty = tabs.some((tab) => tabHasLivePty(state.ptyIdsByTabId, tab.id))
if (hasLivePty) {
const launchAgent = opts?.startup?.launchAgent ?? opts?.agent ?? undefined
if (
launchAgent &&
tabs.some(
(tab) =>
tab.launchAgent === launchAgent &&
(isWebTerminalSurfaceTabId(tab.id) || tabHasLivePty(state.ptyIdsByTabId, tab.id))
)
) {
return
}
const hasMirroredHostTabs = tabs.some((tab) => isWebTerminalSurfaceTabId(tab.id))
if (hasMirroredHostTabs) {
// Why: the host session still owns these tabs — wait for the mirror to repopulate PTY handles instead of duplicating a terminal.
return
}
if (!launchAgent) {
const hasLivePty = tabs.some((tab) => tabHasLivePty(state.ptyIdsByTabId, tab.id))
if (hasLivePty) {
return
}
if (getLastKnownHostTerminalTabCount(runtimeEnvironmentId, worktreeId) > 0) {
return
}
const hasMirroredHostTabs = tabs.some((tab) => isWebTerminalSurfaceTabId(tab.id))
if (hasMirroredHostTabs) {
// Why: the host session still owns these tabs — wait for the mirror to repopulate PTY handles instead of duplicating a terminal.
return
}
const { renderableTabCount } = state.reconcileWorktreeTabModel(worktreeId)
if (tabs.length > 0 && renderableTabCount === 0) {
return
if (getLastKnownHostTerminalTabCount(runtimeEnvironmentId, worktreeId) > 0) {
return
}
const { renderableTabCount } = state.reconcileWorktreeTabModel(worktreeId)
if (tabs.length > 0 && renderableTabCount === 0) {
return
}
}
if (!beginWebRuntimeWakeTerminalRespawn(worktreeId)) {
return
}
// Why: sleep keeps tab rows but terminal.stop clears host PTYs, so a woke workspace can have tab chrome but no surface.
const startup = opts?.startup
const viewModeProps = launchAgent
? initialAgentTabViewModeProps(state.settings, {
agent: launchAgent,
...draftViewModeProps(resolveStartupLaunchDraftText(startup)),
nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable(
getConnectionId(worktreeId)
)
})
: {}
// Why: sleep keeps tab rows but terminal.stop clears host PTYs, while a failed create receipt leaves a selected agent with no host surface.
void createWebRuntimeSessionTerminal({
worktreeId,
environmentId: runtimeEnvironmentId,
activate: true,
...viewModeProps,
...(startup
? {
command: startup.command,
...(startup.env ? { env: startup.env } : {}),
...(startup.launchConfig ? { launchConfig: startup.launchConfig } : {}),
...(startup.launchToken ? { launchToken: startup.launchToken } : {}),
...(launchAgent ? { launchAgent, preparedAgentCommand: true } : {}),
...(startup.startupCommandDelivery
? { startupCommandDelivery: startup.startupCommandDelivery }
: {})
}
: launchAgent
? { agent: launchAgent }
: {}),
activate: opts?.activate !== false,
selectWorktree: false
}).finally(() => {
endWebRuntimeWakeTerminalRespawn(worktreeId)
})
.then((outcome) => {
if (outcome.status === 'failed') {
toast.error(outcome.message, {
id: `web-runtime-worktree-terminal:${runtimeEnvironmentId}:${worktreeId}`
})
}
})
.finally(() => {
endWebRuntimeWakeTerminalRespawn(worktreeId)
})
}
@@ -32,6 +32,23 @@ function seedClosedLastTerminal(worktreeId: string): void {
}
describe('activating a workspace whose last terminal was closed', () => {
it.each([
['Blank Terminal', null, 1],
['an agent', 'codex' as const, 0]
])('seeds a default shell for %s selection only', (_label, agent, expectedTabCount) => {
const worktree = makeWorktree()
seedEmptyActivatableWorktree(worktree)
const result = activateAndRevealWorktree(worktree.id, {
agent,
notifyHostRuntime: false
})
expect(result).not.toBe(false)
expect(result === false ? null : result.primaryTabId === null).toBe(expectedTabCount === 0)
expect(useAppStore.getState().tabsByWorktree[worktree.id] ?? []).toHaveLength(expectedTabCount)
})
it.each([true, false])(
'forwards providesInitialSurface=%s through the async activation gate',
async (providesInitialSurface) => {
@@ -234,6 +251,22 @@ function seedEmptiedFolderWorkspaceOnTwoHosts(): void {
}
describe('activating a folder workspace whose last terminal was closed', () => {
it.each([
['Blank Terminal', null, 1],
['an agent', 'codex' as const, 0]
])('seeds a default shell for %s selection only', (_label, agent, expectedTabCount) => {
seedEmptiedFolderWorkspaceOnTwoHosts()
const result = activateAndRevealFolderWorkspace(FOLDER_ID, {
agent,
executionHostId: 'local'
})
expect(result).not.toBe(false)
expect(useAppStore.getState().activeWorktreeId).toBe(FOLDER_KEY)
expect(useAppStore.getState().tabsByWorktree[FOLDER_KEY]).toHaveLength(expectedTabCount)
})
it.each([true, false])(
'forwards providesInitialSurface=%s through the async activation gate',
async (providesInitialSurface) => {
@@ -6,6 +6,9 @@ import { resetWebRuntimeWakeTerminalRespawnForTests } from '@/runtime/web-runtim
import { resetWebSessionTabsSnapshotFreshnessForTests } from '@/runtime/web-session-tabs-sync'
import { useAppStore } from '@/store'
import { ensureWebRuntimeWorktreeTerminalAfterWake } from './web-runtime-worktree-terminal-after-wake'
import { toast } from 'sonner'
vi.mock('sonner', () => ({ toast: { error: vi.fn() } }))
const initialAppStoreState = useAppStore.getState()
const WORKTREE_PATH = path.join('workspace', 'feature')
@@ -13,6 +16,7 @@ const REPO_PATH = path.join('workspace', 'repo')
const ORCA_WORKSPACES_PATH = path.join('workspace', '.orca-workspaces')
afterEach(() => {
vi.clearAllMocks()
delete (globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__
vi.unstubAllGlobals()
resetWebSessionTabsSnapshotFreshnessForTests()
@@ -115,5 +119,55 @@ describe('empty remote worktree activation', () => {
})
})
)
expect(toast.error).not.toHaveBeenCalled()
})
it('surfaces a failed host terminal request without retrying ambiguously', async () => {
const worktree = makeWorktree()
const callRuntimeEnvironment = vi.fn().mockResolvedValueOnce({
ok: false,
error: { code: 'terminal_create_failed', message: 'Host refused the terminal' }
})
;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true
vi.stubGlobal('window', {
api: {
runtimeEnvironments: {
call: callRuntimeEnvironment,
subscribe: vi.fn()
}
}
})
useAppStore.setState({
repos: [
{
id: 'repo-1',
path: REPO_PATH,
displayName: 'repo',
badgeColor: '#000000',
addedAt: 0
}
],
worktreesByRepo: { 'repo-1': [worktree] },
tabsByWorktree: {},
ptyIdsByTabId: {},
settings: {
...getDefaultSettings(ORCA_WORKSPACES_PATH),
activeRuntimeEnvironmentId: 'web-runtime-1'
},
reconcileWorktreeTabModel: vi.fn(() => ({
renderableTabCount: 0,
activeRenderableTabId: null
}))
})
ensureWebRuntimeWorktreeTerminalAfterWake(worktree.id)
await vi.waitFor(() =>
expect(toast.error).toHaveBeenCalledWith('Host refused the terminal', {
id: `web-runtime-worktree-terminal:web-runtime-1:${worktree.id}`
})
)
expect(callRuntimeEnvironment).toHaveBeenCalledTimes(1)
})
})
@@ -23,7 +23,10 @@ const SURFACE_PROVIDING_CALLERS = [
]
// The activation seam itself: declares the option and forwards it into the tombstone gate.
const SEAM_FILES = ['src/renderer/src/lib/worktree-activation.ts']
const SEAM_FILES = [
'src/renderer/src/lib/worktree-activation-surface-selection.ts',
'src/renderer/src/lib/worktree-activation.ts'
]
function listSourceFiles(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
@@ -0,0 +1,39 @@
import type { TuiAgent } from '../../../shared/tui-agent'
import type {
WorktreeDefaultTabsLaunch,
WorktreeSetupLaunch
} from '../../../shared/worktree/launch-types'
import type { ExecutionHostId } from '../../../shared/execution-host'
import type { PendingSidebarWorktreeReveal } from '@/store/slices/ui'
import type { WorktreeStartupPayload } from '@/lib/worktree-startup-payload'
import type { IssueCommandLaunch } from '@/lib/worktree-setup-issue-command-queue'
export type WorktreeActivationSurfaceSelection = {
/** The create picker's selection; null means Blank Terminal. */
agent?: TuiAgent | null
/** A navigation caller is about to open its own editor, diff, or other non-terminal surface. */
providesInitialSurface?: boolean
}
export type WorktreeActivationOptions = WorktreeActivationSurfaceSelection & {
startup?: WorktreeStartupPayload
initialCwd?: string
setup?: WorktreeSetupLaunch
defaultTabs?: WorktreeDefaultTabsLaunch
issueCommand?: IssueCommandLaunch
sidebarRevealBehavior?: PendingSidebarWorktreeReveal['behavior']
notifyHostRuntime?: boolean
revealInSidebar?: boolean
executionHostId?: ExecutionHostId
backendStartupTerminalSpawned?: boolean
/** Install a preserved fallback startup beside setup/default terminals already seeded. */
createNewTerminalForStartup?: boolean
/** Keep sidebar filters intact when navigating to a hidden target. */
clearSidebarFilters?: boolean
}
export function activationProvidesInitialSurface(
selection?: WorktreeActivationSurfaceSelection
): boolean {
return selection?.providesInitialSurface === true || selection?.agent != null
}
@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { activateAndRevealFolderWorkspace, activateAndRevealWorktree } from './worktree-activation'
import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-seeding'
import type { AppStoreState } from './worktree-activation-test-harness'
import {
@@ -6,9 +7,228 @@ import {
registerWorktreeActivationReset
} from './worktree-activation-test-harness'
import { useAppStore } from '@/store'
import {
makeCreatedAgentWorktree,
seedEmptyActivatableWorktree
} from './worktree-activation-created-agent-test-state'
import {
resetWebRuntimeWakeTerminalRespawnForTests,
shouldSkipWebRuntimeWakeTerminalRespawn
} from '@/runtime/web-runtime-wake-terminal-respawn'
registerWorktreeActivationReset()
afterEach(() => {
vi.unstubAllGlobals()
resetWebRuntimeWakeTerminalRespawnForTests()
})
describe('activateAndRevealWorktree', () => {
it('asks the paired host for the prepared agent terminal when backend startup did not spawn', async () => {
const worktree = {
...makeCreatedAgentWorktree(),
hostId: 'local' as const,
runtimeOwnerEnvironmentId: 'web-runtime-1'
}
const callRuntimeEnvironment = vi.fn(
async (request: { method: string; params?: Record<string, unknown> }) =>
request.method === 'session.tabs.createTerminal'
? {
ok: true,
result: {
tab: { id: 'host-agent-tab', leafId: 'host-agent-leaf' },
publicationEpoch: 'epoch-1',
snapshotVersion: 1
}
}
: { ok: false, error: { code: 'test', message: 'stop after recording the request' } }
)
;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true
vi.stubGlobal('window', {
api: { runtimeEnvironments: { call: callRuntimeEnvironment } }
})
seedEmptyActivatableWorktree(worktree)
const settings = useAppStore.getState().settings
useAppStore.setState({
settings: settings
? { ...settings, activeRuntimeEnvironmentId: 'web-runtime-1' }
: ({ activeRuntimeEnvironmentId: 'web-runtime-1' } as unknown as typeof settings)
})
useAppStore.setState({
tabsByWorktree: {
[worktree.id]: [
{
id: 'stale-local-agent-tab',
ptyId: 'stale-local-agent-pty',
worktreeId: worktree.id,
title: 'Codex',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1,
launchAgent: 'codex'
}
]
}
})
activateAndRevealWorktree(worktree.id, {
agent: 'codex',
startup: {
command: "codex 'fix the ownership race'",
env: { ORCA_AGENT_PROFILE: 'review' },
launchAgent: 'codex',
launchToken: 'launch-1'
}
})
await vi.waitFor(() =>
expect(callRuntimeEnvironment).toHaveBeenCalledWith(
expect.objectContaining({ method: 'worktree.activate' })
)
)
const createRequests = callRuntimeEnvironment.mock.calls.filter(
([request]) => request.method === 'session.tabs.createTerminal'
)
expect(createRequests).toHaveLength(1)
expect(createRequests[0]?.[0]).toEqual(
expect.objectContaining({
params: expect.objectContaining({
command: "codex 'fix the ownership race'",
env: { ORCA_AGENT_PROFILE: 'review' },
launchAgent: 'codex',
launchToken: 'launch-1'
})
})
)
await vi.waitFor(() => expect(shouldSkipWebRuntimeWakeTerminalRespawn(worktree.id)).toBe(false))
})
it('does not request another host terminal when backend startup already spawned', async () => {
const worktree = {
...makeCreatedAgentWorktree(),
hostId: 'local' as const,
runtimeOwnerEnvironmentId: 'web-runtime-1'
}
const callRuntimeEnvironment = vi.fn().mockResolvedValue({
ok: false,
error: { code: 'test', message: 'stop after recording the request' }
})
;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true
vi.stubGlobal('window', {
api: { runtimeEnvironments: { call: callRuntimeEnvironment } }
})
seedEmptyActivatableWorktree(worktree)
useAppStore.setState((state) => ({
settings: state.settings
? { ...state.settings, activeRuntimeEnvironmentId: 'web-runtime-1' }
: ({ activeRuntimeEnvironmentId: 'web-runtime-1' } as unknown as typeof state.settings)
}))
activateAndRevealWorktree(worktree.id, {
agent: 'codex',
backendStartupTerminalSpawned: true
})
await vi.waitFor(() =>
expect(callRuntimeEnvironment).toHaveBeenCalledWith(
expect.objectContaining({ method: 'worktree.activate' })
)
)
expect(
callRuntimeEnvironment.mock.calls.filter(
([request]) => request.method === 'session.tabs.createTerminal'
)
).toHaveLength(0)
})
})
describe('activateAndRevealFolderWorkspace', () => {
it.each([
[
'the selected agent',
{
agent: 'codex' as const,
startup: { command: 'codex', launchAgent: 'codex' as const, launchToken: 'launch-1' }
},
{ command: 'codex', launchAgent: 'codex', launchToken: 'launch-1' }
],
['Blank Terminal', { agent: null }, { command: undefined }]
])('asks the runtime owner for exactly one %s surface', async (_label, activation, expected) => {
const callRuntimeEnvironment = vi.fn(
async (request: { method: string; params?: Record<string, unknown> }) =>
request.method === 'session.tabs.createTerminal'
? {
ok: true,
result: {
tab: { id: 'host-tab', leafId: 'host-leaf' },
publicationEpoch: 'epoch-1',
snapshotVersion: 1
}
}
: { ok: false, error: { code: 'test', message: 'stop after recording the request' } }
)
;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true
vi.stubGlobal('window', {
api: { runtimeEnvironments: { call: callRuntimeEnvironment } }
})
const settings = useAppStore.getState().settings
useAppStore.setState({
activeView: 'terminal',
folderWorkspaces: [
{
id: 'folder-1',
projectGroupId: 'group-1',
name: 'runtime folder',
folderPath: '/workspace/runtime-folder',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
executionHostId: 'runtime:web-runtime-1'
}
],
getFreshFolderWorkspacePathStatus: vi.fn(() => ({ exists: true })),
tabsByWorktree: {},
settings: settings
? { ...settings, activeRuntimeEnvironmentId: 'web-runtime-1' }
: ({ activeRuntimeEnvironmentId: 'web-runtime-1' } as unknown as typeof settings),
markWorktreeVisited: vi.fn(),
recordWorktreeVisit: vi.fn(),
revealWorktreeInSidebar: vi.fn()
} as unknown as Partial<AppStoreState>)
activateAndRevealFolderWorkspace('folder-1', {
...activation,
runtimeEnvironmentId: 'web-runtime-1'
})
await vi.waitFor(() =>
expect(
callRuntimeEnvironment.mock.calls.filter(
([request]) => request.method === 'session.tabs.createTerminal'
)
).toHaveLength(1)
)
const createRequest = callRuntimeEnvironment.mock.calls.find(
([request]) => request.method === 'session.tabs.createTerminal'
)?.[0]
expect(createRequest).toEqual(
expect.objectContaining({
params: expect.objectContaining(expected)
})
)
if (activation.agent === null) {
expect(createRequest?.params).not.toHaveProperty('launchAgent')
}
await vi.waitFor(() =>
expect(shouldSkipWebRuntimeWakeTerminalRespawn('folder:folder-1')).toBe(false)
)
})
})
describe('ensureWorktreeHasInitialTerminal', () => {
it('does not create a local fallback tab in the paired web runtime client', () => {
;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true
+33 -44
View File
@@ -1,8 +1,4 @@
import type { FolderWorkspace } from '../../../shared/folder-workspace-types'
import type {
WorktreeDefaultTabsLaunch,
WorktreeSetupLaunch
} from '../../../shared/worktree/launch-types'
import { translate } from '@/i18n/i18n'
import { useAppStore } from '@/store'
import type { PendingSidebarWorktreeReveal } from '@/store/slices/ui'
@@ -29,13 +25,17 @@ import { isDetachedHeadWorkspace } from '@/components/sidebar/visible-worktrees'
import type { ExecutionHostId } from '../../../shared/execution-host'
import { findFolderWorkspaceOwner } from './folder-workspace-runtime-owner'
import type { WorktreeStartupPayload } from '@/lib/worktree-startup-payload'
import type { IssueCommandLaunch } from '@/lib/worktree-setup-issue-command-queue'
import {
ensureWorktreeHasInitialTerminal,
reseedGatedEmptyWorkspace
} from '@/lib/worktree-initial-terminal-seeding'
import { ensureWebRuntimeWorktreeTerminalAfterWake } from '@/lib/web-runtime-worktree-terminal-after-wake'
import { applyWorktreeNavViewEntry } from '@/lib/worktree-nav-view-history-replay'
import {
activationProvidesInitialSurface,
type WorktreeActivationOptions,
type WorktreeActivationSurfaceSelection
} from './worktree-activation-surface-selection'
/**
* Shared activation sequence used by the worktree palette and add-repo/worktree dialogs.
@@ -80,14 +80,12 @@ function canInspectAgentActivationInventory(): boolean {
export function activateAndRevealFolderWorkspace(
folderWorkspaceId: string,
opts?: {
opts?: WorktreeActivationSurfaceSelection & {
sidebarRevealBehavior?: PendingSidebarWorktreeReveal['behavior']
revealInSidebar?: boolean
startup?: WorktreeStartupPayload
runtimeEnvironmentId?: string | null
executionHostId?: ExecutionHostId
/** See activateAndRevealWorktree — same contract for folder workspaces. */
providesInitialSurface?: boolean
}
): ActivateAndRevealResult | false {
const state = useAppStore.getState()
@@ -133,6 +131,7 @@ export function activateAndRevealFolderWorkspace(
state.setActiveFolderWorkspace(folderWorkspaceId, opts?.executionHostId)
const workspaceKey = folderWorkspaceKey(folderWorkspaceId)
const providesInitialSurface = activationProvidesInitialSurface(opts)
state.markWorktreeVisited(workspaceKey)
if (!state.isNavigatingHistory) {
state.recordWorktreeVisit(workspaceKey)
@@ -151,17 +150,13 @@ export function activateAndRevealFolderWorkspace(
if (shouldGateAgentActivation) {
void gateWorktreeAgentActivation(workspaceKey).then((outcome) => {
if (outcome === 'empty') {
reseedGatedEmptyWorkspace(workspaceKey, opts?.providesInitialSurface)
reseedGatedEmptyWorkspace(workspaceKey, providesInitialSurface)
}
})
}
const primaryTabId = shouldGateAgentActivation
? null
: ensureFolderWorkspaceInitialTerminal(
folderWorkspace,
opts?.startup,
opts?.providesInitialSurface
)
: ensureFolderWorkspaceInitialTerminal(folderWorkspace, opts?.startup, providesInitialSurface)
if (opts?.revealInSidebar !== false) {
state.revealWorktreeInSidebar(
@@ -170,33 +165,20 @@ export function activateAndRevealFolderWorkspace(
)
}
if (opts?.providesInitialSurface !== true) {
ensureWebRuntimeWorktreeTerminalAfterWake(workspaceKey, {
runtimeEnvironmentId,
startup: opts?.startup,
agent: opts?.agent
})
}
return { primaryTabId }
}
export function activateAndRevealWorktree(
worktreeId: string,
opts?: {
startup?: WorktreeStartupPayload
initialCwd?: string
setup?: WorktreeSetupLaunch
defaultTabs?: WorktreeDefaultTabsLaunch
issueCommand?: IssueCommandLaunch
sidebarRevealBehavior?: PendingSidebarWorktreeReveal['behavior']
notifyHostRuntime?: boolean
revealInSidebar?: boolean
executionHostId?: ExecutionHostId
backendStartupTerminalSpawned?: boolean
/** Install a preserved fallback startup beside setup/default terminals already seeded. */
createNewTerminalForStartup?: boolean
/** Set by callers that navigate here only to open their own non-terminal surface
* (an editor file, a diff). Activation then leaves a closed-last-terminal workspace
* empty instead of adding a shell the user never asked for. Caveat: on a
* runtime-owned workspace with a live web session the host owns terminal creation,
* so ensureWebRuntimeWorktreeTerminalAfterWake may still seed one (matches main). */
providesInitialSurface?: boolean
/** Keep sidebar filters intact when navigating to a hidden target. */
clearSidebarFilters?: boolean
}
opts?: WorktreeActivationOptions
): ActivateAndRevealResult | false {
const state = useAppStore.getState()
const wt = state.getKnownWorktreeById(worktreeId, opts?.executionHostId)
@@ -206,6 +188,7 @@ export function activateAndRevealWorktree(
const hasActivationWork = Boolean(
opts?.startup || opts?.setup || opts?.defaultTabs || opts?.issueCommand
)
const providesInitialSurface = activationProvidesInitialSurface(opts)
// Why: a plain reselect should still reveal the sidebar row but must not restamp focus recency or wake persistence.
const isPlainAlreadyActiveTerminal =
!hasActivationWork &&
@@ -266,7 +249,7 @@ export function activateAndRevealWorktree(
if (shouldGateAgentActivation) {
void gateWorktreeAgentActivation(worktreeId).then((outcome) => {
if (outcome === 'empty') {
reseedGatedEmptyWorkspace(worktreeId, opts?.providesInitialSurface)
reseedGatedEmptyWorkspace(worktreeId, providesInitialSurface)
}
})
}
@@ -274,7 +257,7 @@ export function activateAndRevealWorktree(
// 4. Ensure a focusable surface exists for externally-created worktrees
const primaryTabId = shouldGateAgentActivation
? null
: opts?.providesInitialSurface === true && !hasActivationWork
: providesInitialSurface && !hasActivationWork
? null
: ensureWorktreeHasInitialTerminal(
useAppStore.getState(),
@@ -286,8 +269,8 @@ export function activateAndRevealWorktree(
{
...(opts?.backendStartupTerminalSpawned ? { backendStartupTerminalSpawned: true } : {}),
...(opts?.createNewTerminalForStartup ? { createNewTerminalForStartup: true } : {}),
...(opts?.providesInitialSurface === true ? { callerProvidesSurface: true } : {}),
reseedEmptiedWorkspace: opts?.providesInitialSurface !== true
...(providesInitialSurface ? { callerProvidesSurface: true } : {}),
reseedEmptiedWorkspace: !providesInitialSurface
}
)
if (primaryTabId && opts?.initialCwd) {
@@ -325,8 +308,15 @@ export function activateAndRevealWorktree(
}
}
if (opts?.notifyHostRuntime !== false && !opts?.backendStartupTerminalSpawned) {
ensureWebRuntimeWorktreeTerminalAfterWake(worktreeId)
if (
opts?.notifyHostRuntime !== false &&
!opts?.backendStartupTerminalSpawned &&
opts?.providesInitialSurface !== true
) {
ensureWebRuntimeWorktreeTerminalAfterWake(worktreeId, {
startup: opts?.startup,
agent: opts?.agent
})
}
return { primaryTabId }
@@ -340,9 +330,8 @@ export function activateAndRevealWorktree(
*/
export function activateAndRevealWorkspace(
workspaceId: string,
opts?: {
opts?: WorktreeActivationSurfaceSelection & {
executionHostId?: ExecutionHostId
providesInitialSurface?: boolean
revealInSidebar?: boolean
/** Worktree-only: folder workspaces are never filter-hidden. */
clearSidebarFilters?: boolean
@@ -0,0 +1,87 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { WorktreeCreationRequest } from './pending-worktree-creation'
const mocks = vi.hoisted(() => ({
activateAndRevealWorktree: vi.fn(),
completeWorktreeCreation: vi.fn(),
ensureWorktreeHasInitialTerminal: vi.fn(),
ensureWebRuntimeWorktreeTerminalAfterWake: vi.fn()
}))
const store = {
activePendingCreationId: null as string | null,
activeView: 'tasks' as 'tasks' | 'terminal',
createWorktree: vi.fn(),
pendingWorktreeCreations: {} as Record<string, unknown>,
repos: []
}
vi.mock('@/store', () => ({ useAppStore: { getState: () => store } }))
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealWorktree: mocks.activateAndRevealWorktree
}))
vi.mock('@/lib/worktree-initial-terminal-seeding', () => ({
ensureWorktreeHasInitialTerminal: mocks.ensureWorktreeHasInitialTerminal
}))
vi.mock('@/lib/worktree-creation-completion', () => ({
completeWorktreeCreation: mocks.completeWorktreeCreation
}))
vi.mock('@/lib/web-runtime-worktree-terminal-after-wake', () => ({
ensureWebRuntimeWorktreeTerminalAfterWake: mocks.ensureWebRuntimeWorktreeTerminalAfterWake
}))
import { executeWorktreeCreation } from './worktree-creation-flow-execute'
const request: WorktreeCreationRequest = {
repoId: 'repo-1',
name: 'feature',
setupDecision: 'inherit',
agent: 'codex',
agentLaunchRoute: 'terminal-tui',
pendingFirstAgentMessageRename: false,
note: '',
startupPlan: null,
quickPrompt: '',
quickTelemetry: null
}
describe('executeWorktreeCreation agent seeding', () => {
beforeEach(() => {
vi.clearAllMocks()
store.activePendingCreationId = null
store.activeView = 'tasks'
store.pendingWorktreeCreations = { 'creation-1': { creationId: 'creation-1' } }
store.createWorktree.mockResolvedValue({
worktree: { id: 'worktree-1', repoId: request.repoId }
})
})
it('routes a background agent selection through host-aware surface creation', async () => {
await executeWorktreeCreation('creation-1', request)
expect(mocks.activateAndRevealWorktree).not.toHaveBeenCalled()
expect(mocks.ensureWorktreeHasInitialTerminal).not.toHaveBeenCalled()
expect(mocks.ensureWebRuntimeWorktreeTerminalAfterWake).toHaveBeenCalledOnce()
expect(mocks.ensureWebRuntimeWorktreeTerminalAfterWake).toHaveBeenCalledWith('worktree-1', {
startup: undefined,
agent: 'codex',
activate: false
})
expect(mocks.completeWorktreeCreation).toHaveBeenCalledWith(
expect.objectContaining({ primaryTabId: null })
)
})
it('passes the agent selection through an active reveal', async () => {
store.activePendingCreationId = 'creation-1'
store.activeView = 'terminal'
mocks.activateAndRevealWorktree.mockReturnValue({ primaryTabId: null })
await executeWorktreeCreation('creation-1', request)
expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith(
'worktree-1',
expect.objectContaining({ agent: 'codex' })
)
})
})
@@ -22,6 +22,7 @@ import { buildWorktreeCreationStartupOpt } from '@/lib/worktree-creation-flow-st
import { launchStructuredWorktreeSession } from '@/lib/worktree-creation-structured-session'
import { completeWorktreeCreation } from '@/lib/worktree-creation-completion'
import { markStructuredWorktreeLaunchUnconfirmed } from '@/lib/worktree-creation-structured-recovery'
import { ensureWebRuntimeWorktreeTerminalAfterWake } from '@/lib/web-runtime-worktree-terminal-after-wake'
// Why: activePendingCreationId can outlive the terminal route when the user
// switches app views; only the terminal route renders the creation panel.
@@ -168,6 +169,7 @@ export async function executeWorktreeCreation(
if (shouldActivateOnCompletion && !structuredLaunch) {
activation = activateAndRevealWorktree(worktree.id, {
sidebarRevealBehavior: 'auto',
...(preparedRequest.agent !== null ? { agent: preparedRequest.agent } : {}),
...(result.setup ? { setup: result.setup } : {}),
...(result.defaultTabs ? { defaultTabs: result.defaultTabs } : {}),
...(startupOpt ? { startup: startupOpt } : {}),
@@ -181,7 +183,7 @@ export async function executeWorktreeCreation(
startupOpt || result.setup || preparedRequest.issueCommand || result.defaultTabs
)
primaryTabId =
structuredLaunch && !hasExplicitTerminalWork
preparedRequest.agent !== null && !hasExplicitTerminalWork
? null
: ensureWorktreeHasInitialTerminal(
useAppStore.getState(),
@@ -192,10 +194,17 @@ export async function executeWorktreeCreation(
result.defaultTabs,
{
activateCreatedTabs: false,
...(structuredLaunch ? { callerProvidesSurface: true } : {}),
...(preparedRequest.agent !== null ? { callerProvidesSurface: true } : {}),
...(backendSpawned ? { backendStartupTerminalSpawned: true } : {})
}
)
if (!structuredLaunch && !backendSpawned) {
ensureWebRuntimeWorktreeTerminalAfterWake(worktree.id, {
startup: startupOpt,
agent: preparedRequest.agent,
activate: false
})
}
}
let structuredLaunchAccepted = structuredLaunch
@@ -606,7 +606,7 @@ describe('staged background worktree creation', () => {
delete store.pendingWorktreeCreations['creation-1']
store.activePendingCreationId = null
resolveTrust()
await vi.waitFor(() => expect(ensureWorktreeHasInitialTerminal).toHaveBeenCalledTimes(1))
await vi.waitFor(() => expect(store.removePendingWorktreeCreation).toHaveBeenCalled())
expect(activateAndRevealWorktree).not.toHaveBeenCalled()
})
@@ -13,6 +13,7 @@ const mocks = vi.hoisted(() => ({
activateStructuredAgentSessionById: vi.fn(),
activateAndRevealWorktree: vi.fn(),
ensureWorktreeHasInitialTerminal: vi.fn(),
ensureWebRuntimeWorktreeTerminalAfterWake: vi.fn(),
preflightAgentTrust: vi.fn(),
updateWorktreeMeta: vi.fn()
}))
@@ -54,6 +55,10 @@ vi.mock('@/lib/worktree-initial-terminal-seeding', () => ({
ensureWorktreeHasInitialTerminal: mocks.ensureWorktreeHasInitialTerminal
}))
vi.mock('@/lib/web-runtime-worktree-terminal-after-wake', () => ({
ensureWebRuntimeWorktreeTerminalAfterWake: mocks.ensureWebRuntimeWorktreeTerminalAfterWake
}))
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealWorktree: mocks.activateAndRevealWorktree
}))
@@ -351,6 +356,11 @@ describe('launchStructuredWorktreeSession', () => {
undefined,
{ activateCreatedTabs: false, createNewTerminalForStartup: true }
)
expect(mocks.ensureWebRuntimeWorktreeTerminalAfterWake).toHaveBeenCalledWith('worktree-1', {
startup: undefined,
agent: 'codex',
activate: false
})
})
it('stops the fallback mid-way when the creation is dismissed and retires nothing', async () => {
@@ -12,6 +12,7 @@ import type { WorktreeStartupPayload } from '@/lib/worktree-startup-payload'
import { closeStructuredAgentSession } from '@/runtime/structured-agent-session-close'
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector'
import { ensureWebRuntimeWorktreeTerminalAfterWake } from '@/lib/web-runtime-worktree-terminal-after-wake'
export type WorktreeCreationStructuredSessionResult = {
accepted: boolean
@@ -92,17 +93,21 @@ async function openLegacyWorktreeSurface(
})
return { activation, primaryTabId: activation === false ? null : activation.primaryTabId }
}
return {
primaryTabId: ensureWorktreeHasInitialTerminal(
useAppStore.getState(),
args.worktreeId,
args.fallbackStartupOpt,
undefined,
undefined,
undefined,
{ activateCreatedTabs: false, createNewTerminalForStartup: true }
)
}
const primaryTabId = ensureWorktreeHasInitialTerminal(
useAppStore.getState(),
args.worktreeId,
args.fallbackStartupOpt,
undefined,
undefined,
undefined,
{ activateCreatedTabs: false, createNewTerminalForStartup: true }
)
ensureWebRuntimeWorktreeTerminalAfterWake(args.worktreeId, {
startup: args.fallbackStartupOpt,
agent: args.request.agent,
activate: false
})
return { primaryTabId }
}
export async function launchStructuredWorktreeSession(
@@ -28,6 +28,8 @@ export type CreateWebRuntimeSessionTerminalArgs = {
launchToken?: string
agent?: TuiAgent
launchAgent?: TuiAgent
/** The command already encodes the complete agent startup and prompt-delivery plan. */
preparedAgentCommand?: boolean
agentSessionKind?: 'fresh' | 'resume'
prompt?: string
promptDelivery?: AgentPromptDelivery
@@ -88,7 +88,9 @@ export async function createWebRuntimeSessionTerminalResult(
let legacyAlreadyPlacedInGroup = false
// Why: structured creation cannot yet express afterTabId; keep the exact legacy placement contract until it can.
// Why: focus belongs to the paired client; a headless execution host has no renderer to focus.
const hostAuthority = args.afterTabId
// Why: rebuilding a prepared command through host authority can discard its embedded prompt and delivery flags.
const mustUseLegacyAgentCreate = args.preparedAgentCommand || args.afterTabId
const hostAuthority = mustUseLegacyAgentCreate
? undefined
: args.agentSessionKind === 'resume'
? args.providerSession
@@ -141,13 +141,16 @@ export function installActiveSessionTabsSubscription({
const hasLiveLocalPty = localTabs.some(
(tab) => (syncState.ptyIdsByTabId[tab.id] ?? []).length > 0
)
const bootstrap = shouldBootstrapInitialWebRuntimeTerminal({
event: recoveredEvent,
activeWorktreeId,
requestedInitialTerminal,
snapshotIsFresh: decision.apply,
localTerminalCount
})
const skipAutomaticTerminal = shouldSkipWebRuntimeWakeTerminalRespawn(activeWorktreeId)
const bootstrap =
!skipAutomaticTerminal &&
shouldBootstrapInitialWebRuntimeTerminal({
event: recoveredEvent,
activeWorktreeId,
requestedInitialTerminal,
snapshotIsFresh: decision.apply,
localTerminalCount
})
const respawn = shouldRespawnWebRuntimeTerminalAfterWake({
event: recoveredEvent,
activeWorktreeId,
@@ -155,7 +158,7 @@ export function installActiveSessionTabsSubscription({
snapshotIsFresh: decision.apply,
localTerminalCount,
hasLiveLocalPty,
skipWakeRespawn: shouldSkipWebRuntimeWakeTerminalRespawn(activeWorktreeId)
skipWakeRespawn: skipAutomaticTerminal
})
let settle: HostSessionMirrorSettle | null = decision.apply
? null