fix(sidebar): stop background workspace creation from scrolling the sidebar

Creating a workspace in the background still spawns its terminals, and the
renderer treated "no presentation stated" as "point the user at this
terminal" -- revealing (scrolling to) the owning workspace.

Split adoption from surfacing with an explicit surfaceOwner flag: background
worktree creates and worker dispatch adopt their tabs silently, while
`orca terminal create` keeps its discoverability reveal.
This commit is contained in:
Neil
2026-07-30 01:41:47 -07:00
parent 37af457752
commit aa273e44f2
11 changed files with 394 additions and 20 deletions
+182
View File
@@ -36064,10 +36064,13 @@ describe('OrcaRuntimeService', () => {
(spawn.mock.calls[1]?.[0] as { env?: Record<string, string> } | undefined)?.env ?? {}
expectStablePaneKeyEnv(setupSpawnEnv)
const setupLeafId = setupSpawnEnv.ORCA_PANE_KEY.slice(`${setupSpawnEnv.ORCA_TAB_ID}:`.length)
// Why: a background CLI create adopts its tabs silently — surfaceOwner:false
// keeps the sidebar from scrolling to a workspace the user never asked for.
expect(revealTerminalSession).toHaveBeenLastCalledWith(result.worktree.id, {
ptyId: 'pty-setup',
title: 'Setup',
activate: false,
surfaceOwner: false,
tabId: setupSpawnEnv.ORCA_TAB_ID,
leafId: setupLeafId
})
@@ -36303,15 +36306,194 @@ describe('OrcaRuntimeService', () => {
const initialLeafId = initialSpawnEnv.ORCA_PANE_KEY.slice(
`${initialSpawnEnv.ORCA_TAB_ID}:`.length
)
// Why: the renderer treats a missing surfaceOwner as "reveal the owner", which
// scrolled the sidebar to background CLI creates.
expect(revealTerminalSession).toHaveBeenCalledWith(result.worktree.id, {
ptyId: 'pty-created-worktree',
title: null,
activate: false,
surfaceOwner: false,
tabId: initialSpawnEnv.ORCA_TAB_ID,
leafId: initialLeafId
})
})
it('does not surface the new workspace when a background CLI create launches its agent', async () => {
const runtime = new OrcaRuntimeService(store)
const activateWorktree = vi.fn()
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-background-agent' })
const spawn = vi.fn().mockResolvedValue({ id: 'pty-background-agent' })
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree,
createTerminal: vi.fn(),
revealTerminalSession,
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-background-agent')
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-background-agent')
vi.mocked(getEffectiveHooks).mockReturnValue(null)
vi.mocked(listWorktrees).mockResolvedValue([
{
path: '/tmp/workspaces/runtime-background-agent',
head: 'def',
branch: 'runtime-background-agent',
isBare: false,
isMainWorktree: false
}
])
const result = await runtime.createManagedWorktree({
repoSelector: 'id:repo-1',
name: 'runtime-background-agent',
createdWithAgent: 'claude',
startup: { command: 'claude' }
})
// `orca worktree create --agent claude` without --activate: the agent tab is
// adopted, but the sidebar must stay on whatever the user was reading.
expect(activateWorktree).not.toHaveBeenCalled()
expect(revealTerminalSession).toHaveBeenCalledWith(
result.worktree.id,
expect.objectContaining({
ptyId: 'pty-background-agent',
activate: false,
surfaceOwner: false
})
)
})
it('still surfaces the new workspace when the caller explicitly activates it', async () => {
const runtime = new OrcaRuntimeService(store)
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-activated-agent' })
const spawn = vi.fn().mockResolvedValue({ id: 'pty-activated-agent' })
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree: vi.fn(),
createTerminal: vi.fn(),
revealTerminalSession,
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-activated-agent')
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-activated-agent')
vi.mocked(getEffectiveHooks).mockReturnValue(null)
vi.mocked(listWorktrees).mockResolvedValue([
{
path: '/tmp/workspaces/runtime-activated-agent',
head: 'def',
branch: 'runtime-activated-agent',
isBare: false,
isMainWorktree: false
}
])
const result = await runtime.createManagedWorktree({
repoSelector: 'id:repo-1',
name: 'runtime-activated-agent',
activate: true,
createdWithAgent: 'claude',
startup: { command: 'claude' }
})
// No-regression guard: an explicit --activate still reveals the workspace, so
// surfaceOwner must stay absent.
const revealPayload = revealTerminalSession.mock.calls[0]?.[1] as
| { surfaceOwner?: boolean }
| undefined
expect(revealTerminalSession).toHaveBeenCalledWith(
result.worktree.id,
expect.objectContaining({ ptyId: 'pty-activated-agent' })
)
expect(revealPayload).not.toHaveProperty('surfaceOwner')
})
it('still surfaces the new workspace when hooks run in the foreground', async () => {
const runtime = new OrcaRuntimeService(store)
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-hooks-agent' })
const spawn = vi.fn().mockResolvedValue({ id: 'pty-hooks-agent' })
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree: vi.fn(),
createTerminal: vi.fn(),
revealTerminalSession,
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-hooks-agent')
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-hooks-agent')
vi.mocked(getEffectiveHooks).mockReturnValue(null)
vi.mocked(listWorktrees).mockResolvedValue([
{
path: '/tmp/workspaces/runtime-hooks-agent',
head: 'def',
branch: 'runtime-hooks-agent',
isBare: false,
isMainWorktree: false
}
])
const result = await runtime.createManagedWorktree({
repoSelector: 'id:repo-1',
name: 'runtime-hooks-agent',
runHooks: true,
createdWithAgent: 'claude',
startup: { command: 'claude' }
})
const revealPayload = revealTerminalSession.mock.calls[0]?.[1] as
| { surfaceOwner?: boolean }
| undefined
expect(revealTerminalSession).toHaveBeenCalledWith(
result.worktree.id,
expect.objectContaining({ ptyId: 'pty-hooks-agent' })
)
expect(revealPayload).not.toHaveProperty('surfaceOwner')
})
it('honors split setup placement for CLI-created worktrees without startup agents', async () => {
const runtimeStore = {
...store,
+35 -15
View File
@@ -1271,6 +1271,10 @@ type TerminalCreateOptions = {
rendererBacked?: boolean
activate?: boolean
presentation?: RuntimeTerminalPresentation
// Why: `false` adopts the terminal without pointing the user at it — no
// sidebar reveal, no tab focus. Distinct from 'background' presentation,
// which skips renderer adoption entirely.
surfaceOwner?: boolean
tabId?: string
leafId?: string
sessionId?: string
@@ -1702,6 +1706,7 @@ type RuntimeNotifier = {
viewMode?: 'terminal' | 'chat'
activate?: boolean
presentation?: RuntimeTerminalPresentation
surfaceOwner?: boolean
tabId?: string
leafId?: string
splitFromLeafId?: string
@@ -19942,7 +19947,8 @@ export class OrcaRuntimeService {
private async createDefaultTabTerminals(
worktreeSelector: string,
worktreeId: string,
defaultTabs: CreateWorktreeResult['defaultTabs'] | undefined
defaultTabs: CreateWorktreeResult['defaultTabs'] | undefined,
surfaceOwner?: boolean
): Promise<string[]> {
if (!defaultTabs || defaultTabs.tabs.length === 0 || !this.ptyController?.spawn) {
return []
@@ -19953,7 +19959,8 @@ export class OrcaRuntimeService {
const command = template.command?.trim()
const terminal = await this.createTerminal(worktreeSelector, {
...(template.title ? { title: template.title } : {}),
...(command && defaultTabs.runCommands ? { command } : {})
...(command && defaultTabs.runCommands ? { command } : {}),
...(surfaceOwner === false ? { surfaceOwner: false } : {})
})
handles.push(terminal.handle)
if (template.color && terminal.tabId) {
@@ -19984,17 +19991,22 @@ export class OrcaRuntimeService {
// the setup command. Pass that wrapped command through so the Setup tab runs
// the same script the agent is waiting on instead of a bare runner.
wrappedSetupCommand?: string
// Why: a workspace provisioned in the background must not pull the sidebar
// to itself; the user never asked to look at these tabs.
surfaceOwner?: boolean
}): Promise<{ setupSpawned: boolean; setupTerminalHandle: string | null }> {
if (!this.ptyController?.spawn) {
return { setupSpawned: false, setupTerminalHandle: null }
}
const silent = args.surfaceOwner === false ? ({ surfaceOwner: false } as const) : {}
let setupSpawned = false
let setupTerminalHandle: string | null = null
try {
const defaultTabHandles = await this.createDefaultTabTerminals(
args.worktreeSelector,
args.worktreeId,
args.defaultTabs
args.defaultTabs,
args.surfaceOwner
)
let primaryTerminalHandle = args.primaryTerminalHandle ?? defaultTabHandles[0] ?? null
const setupLaunchMode =
@@ -20004,7 +20016,7 @@ export class OrcaRuntimeService {
>
).setupScriptLaunchMode ?? 'new-tab'
if (!args.hasStartupTerminal && !primaryTerminalHandle) {
const terminal = await this.createTerminal(args.worktreeSelector)
const terminal = await this.createTerminal(args.worktreeSelector, { ...silent })
primaryTerminalHandle = terminal.handle
}
if (args.setup) {
@@ -20035,7 +20047,8 @@ export class OrcaRuntimeService {
: this.createTerminal(args.worktreeSelector, {
title: 'Setup',
command: setupCommand,
env: setupEnv
env: setupEnv,
...silent
}))
setupTerminalHandle = setupTerminal.handle
setupSpawned = true
@@ -20314,7 +20327,8 @@ export class OrcaRuntimeService {
...(effectiveCreatedWithAgent ? { launchAgent: effectiveCreatedWithAgent } : {}),
...(effectiveStartup.viewMode ? { viewMode: effectiveStartup.viewMode } : {}),
startupCommandDelivery: effectiveStartup.startupCommandDelivery,
telemetry: effectiveStartup.telemetry
telemetry: effectiveStartup.telemetry,
...(shouldActivate ? {} : { surfaceOwner: false })
})
if (effectiveDraftPaste) {
this.pasteStartupDraftWhenReady(terminal.handle, effectiveDraftPaste)
@@ -20345,7 +20359,7 @@ export class OrcaRuntimeService {
}
} else if (this.ptyController?.spawn && !didSpawnStartup) {
try {
await this.createTerminal(`id:${worktree.id}`)
await this.createTerminal(`id:${worktree.id}`, { surfaceOwner: false })
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
warning = warning
@@ -21054,7 +21068,8 @@ export class OrcaRuntimeService {
...(effectiveCreatedWithAgent ? { launchAgent: effectiveCreatedWithAgent } : {}),
...(sequencedStartup.viewMode ? { viewMode: sequencedStartup.viewMode } : {}),
startupCommandDelivery: sequencedStartup.startupCommandDelivery,
telemetry: sequencedStartup.telemetry
telemetry: sequencedStartup.telemetry,
...(shouldActivate ? {} : { surfaceOwner: false })
})
if (effectiveDraftPaste) {
this.pasteStartupDraftWhenReady(terminal.handle, effectiveDraftPaste)
@@ -21155,7 +21170,8 @@ export class OrcaRuntimeService {
: 'posix'
: 'posix',
observeSetupCompletion: args.observeSetupCompletion,
...(wrappedSetupCommandStr ? { wrappedSetupCommand: wrappedSetupCommandStr } : {})
...(wrappedSetupCommandStr ? { wrappedSetupCommand: wrappedSetupCommandStr } : {}),
surfaceOwner: false
})
// Why: runtime owns setup spawning here, so the RPC result must omit setup
// to keep the headless/mobile caller from launching it a second time.
@@ -21171,7 +21187,7 @@ export class OrcaRuntimeService {
}
} else if (this.ptyController?.spawn) {
try {
await this.createTerminal(`id:${worktree.id}`)
await this.createTerminal(`id:${worktree.id}`, { surfaceOwner: false })
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
warning = warning
@@ -21355,6 +21371,7 @@ export class OrcaRuntimeService {
this.invalidateWorktreeScanCacheForRepo(repo.id)
this.notifyWorktreesChanged(repo.id)
const shouldActivate = args.activate === true || args.runHooks === true
let warning = result.warning
let didSpawnStartup = false
// Why: same no-double-spawn contract as the local path — once runtime
@@ -21403,7 +21420,8 @@ export class OrcaRuntimeService {
...(args.createdWithAgent ? { launchAgent: args.createdWithAgent } : {}),
...(sequencedStartup.viewMode ? { viewMode: sequencedStartup.viewMode } : {}),
startupCommandDelivery: sequencedStartup.startupCommandDelivery,
telemetry: sequencedStartup.telemetry
telemetry: sequencedStartup.telemetry,
...(shouldActivate ? {} : { surfaceOwner: false })
})
if (args.startupDraftPaste) {
this.pasteStartupDraftWhenReady(terminal.handle, args.startupDraftPaste)
@@ -21424,7 +21442,6 @@ export class OrcaRuntimeService {
}
}
const shouldActivate = args.activate === true || args.runHooks === true
if (shouldActivate) {
const runtimeWillProvisionTerminals =
didSpawnStartup && Boolean(result.setup || result.defaultTabs)
@@ -21506,7 +21523,8 @@ export class OrcaRuntimeService {
: 'posix'
: 'posix',
observeSetupCompletion: args.observeSetupCompletion,
...(wrappedSetupCommandStr ? { wrappedSetupCommand: wrappedSetupCommandStr } : {})
...(wrappedSetupCommandStr ? { wrappedSetupCommand: wrappedSetupCommandStr } : {}),
surfaceOwner: false
})
// Why: runtime owns setup spawning here, so omit setup from the RPC result
// to keep the headless/mobile caller from launching it a second time.
@@ -21522,7 +21540,7 @@ export class OrcaRuntimeService {
}
} else if (!shouldActivate && this.ptyController?.spawn) {
try {
await this.createTerminal(`path:${result.worktree.path}`)
await this.createTerminal(`path:${result.worktree.path}`, { surfaceOwner: false })
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
warning = warning
@@ -23948,6 +23966,7 @@ export class OrcaRuntimeService {
...(launchOpts.viewMode ? { viewMode: launchOpts.viewMode } : {}),
activate: presentation === 'focused',
...(presentation ? { presentation } : {}),
...(opts.surfaceOwner === false ? { surfaceOwner: false } : {}),
tabId,
leafId
})
@@ -24032,7 +24051,8 @@ export class OrcaRuntimeService {
startupCommandDelivery: launchOpts.startupCommandDelivery,
title: launchOpts.title,
activate: presentation === 'focused',
...(presentation ? { presentation } : {})
...(presentation ? { presentation } : {}),
...(opts.surfaceOwner === false ? { surfaceOwner: false } : {})
})
})
@@ -60,7 +60,10 @@ export async function createExistingWorktreeWorkerTerminal(args: {
}): Promise<{ handle: string; warning?: string }> {
const terminal = await args.runtime.createTerminal(`id:${args.worktreeId}`, {
command: args.agent,
title: `worker-${args.taskId}`
title: `worker-${args.taskId}`,
// Why: dispatching a worker is background work; it must not pull the sidebar
// to the worker's workspace while the user is reading somewhere else.
surfaceOwner: false
})
args.effects.push({
kind: 'terminal',
@@ -2176,9 +2176,12 @@ describe('orchestration RPC methods', () => {
)
expect(db.getTask(task.id)?.status).toBe('dispatched')
expect(db.getWorkerDispatch(result.dispatchId)?.state).toBe('ready')
// Why: dispatching a worker is background work — surfaceOwner:false adopts
// the tab without scrolling the sidebar to the worker's workspace.
expect(runtime.createTerminal).toHaveBeenCalledWith('id:repo::worktree', {
command: 'codex',
title: `worker-${task.id}`
title: `worker-${task.id}`,
surfaceOwner: false
})
expect(runtime.sendTerminalAgentPrompt).toHaveBeenCalledWith(
'term_worker',
@@ -2277,7 +2280,9 @@ describe('orchestration RPC methods', () => {
)
expect(runtime.createTerminal).toHaveBeenCalledWith(
'id:repo::other',
expect.objectContaining({ command: 'codex' })
// Why: starting a worker in an existing worktree must not pull the sidebar
// away from whatever the user is looking at.
expect.objectContaining({ command: 'codex', surfaceOwner: false })
)
expect(createWorktree).not.toHaveBeenCalled()
})
@@ -402,6 +402,7 @@ function registerRuntimeWindowLifecycle(
...(opts.viewMode ? { viewMode: opts.viewMode } : {}),
activate: opts.activate !== false,
...(opts.presentation ? { presentation: opts.presentation } : {}),
...(opts.surfaceOwner === false ? { surfaceOwner: false } : {}),
// Why: pre-minted tabId aligns the renderer tab id with the paneKey baked into the PTY env, so hook events route right.
...(opts.tabId !== undefined ? { tabId: opts.tabId } : {}),
...(opts.leafId !== undefined ? { leafId: opts.leafId } : {}),
+1
View File
@@ -3157,6 +3157,7 @@ export type PreloadApi = {
activate?: boolean
focus?: boolean
presentation?: RuntimeTerminalPresentation
surfaceOwner?: boolean
tabId?: string
leafId?: string
splitFromLeafId?: string
+2
View File
@@ -3722,6 +3722,7 @@ const api = {
activate?: boolean
focus?: boolean
presentation?: RuntimeTerminalPresentation
surfaceOwner?: boolean
tabId?: string
leafId?: string
splitFromLeafId?: string
@@ -3747,6 +3748,7 @@ const api = {
activate?: boolean
focus?: boolean
presentation?: RuntimeTerminalPresentation
surfaceOwner?: boolean
tabId?: string
leafId?: string
splitFromLeafId?: string
@@ -8,12 +8,24 @@ export type CreateTerminalRequest = {
command?: string
title?: string
ptyId?: string
activate?: boolean
presentation?: 'background' | 'focused'
surfaceOwner?: boolean
tabId?: string
leafId?: string
splitFromLeafId?: string
}
export type RequestTerminalCreateRequest = {
requestId: string
worktreeId?: string
command?: string
title?: string
activate?: boolean
presentation?: 'background' | 'focused'
surfaceOwner?: boolean
}
export type HarnessTab = { id: string; ptyId?: string | null; title?: string }
export type HarnessStoreState = {
@@ -108,6 +120,7 @@ export type IpcEventsHarness = {
/** Call inside the test body: useIpcEvents runs its effects eagerly here. */
useIpcEvents: () => void
createTerminal: (request: CreateTerminalRequest) => void
requestTerminalCreate: (request: RequestTerminalCreateRequest) => void
replyTerminalCreate: ReturnType<typeof vi.fn>
}
@@ -120,6 +133,7 @@ export async function loadIpcEventsHarness(
): Promise<IpcEventsHarness> {
const replyTerminalCreate = vi.fn()
let createTerminalListener: ((request: CreateTerminalRequest) => void) | null = null
let requestTerminalCreateListener: ((request: RequestTerminalCreateRequest) => void) | null = null
vi.resetModules()
vi.unstubAllGlobals()
@@ -170,6 +184,10 @@ export async function loadIpcEventsHarness(
onCreateTerminal: (listener: (request: CreateTerminalRequest) => void) => {
createTerminalListener = listener
return () => {}
},
onRequestTerminalCreate: (listener: (request: RequestTerminalCreateRequest) => void) => {
requestTerminalCreateListener = listener
return () => {}
}
}),
rateLimits: {
@@ -219,6 +237,12 @@ export async function loadIpcEventsHarness(
}
createTerminalListener(request)
},
requestTerminalCreate: (request) => {
if (typeof requestTerminalCreateListener !== 'function') {
throw new Error('Expected the request-terminal-create listener to be registered')
}
requestTerminalCreateListener(request)
},
replyTerminalCreate
}
}
+127
View File
@@ -11,6 +11,11 @@ import {
resolveBrowserSessionTabTarget,
resolveZoomTarget
} from './useIpcEvents'
import {
createHarnessStoreState,
loadIpcEventsHarness,
type HarnessStoreState
} from './ipc-events-test-harness'
import type { SleepingAgentLaunchConfig } from '../../../shared/agent-session-resume'
import type { AgentStatusClearIpcPayload } from '../../../shared/agent-status-types'
import type { TuiAgent } from '../../../shared/types'
@@ -8555,3 +8560,125 @@ describe('parked terminal recovery on repos:changed', () => {
clearTerminalTabsParkedOnUnresolvedHost()
})
})
describe('useIpcEvents silent terminal adoption (surfaceOwner: false)', () => {
const asMock = (value: unknown): ReturnType<typeof vi.fn> => value as ReturnType<typeof vi.fn>
function createBackgroundWorkspaceState(): HarnessStoreState {
return createHarnessStoreState({
tabsByWorktree: {},
// Honour the pre-minted tab id so adoption resolves the same tab main asked for.
createTab: vi.fn(
(_worktreeId: string, _groupId?: string, _tabType?: string, options?: { id?: string }) => ({
id: options?.id ?? 'tab-minted'
})
),
worktreesByRepo: {
'repo-1': [
{ id: 'wt-1', repoId: 'repo-1' },
{ id: 'wt-2', repoId: 'repo-1' }
]
}
})
}
it('adopts a background workspace terminal without scrolling the sidebar to it', async () => {
const storeState = createBackgroundWorkspaceState()
const harness = await loadIpcEventsHarness(storeState)
harness.useIpcEvents()
// Control: `orca terminal create` (no surfaceOwner) keeps its reveal so the
// user can find the terminal they just asked for.
harness.createTerminal({
worktreeId: 'wt-2',
ptyId: 'pty-cli',
activate: false,
tabId: 'tab-cli'
})
expect(storeState.createTab).toHaveBeenCalledWith('wt-2', undefined, undefined, {
initialPtyId: 'pty-cli',
activate: false,
id: 'tab-cli'
})
expect(storeState.revealWorktreeInSidebar).toHaveBeenCalledWith('wt-2')
asMock(storeState.createTab).mockClear()
asMock(storeState.revealWorktreeInSidebar).mockClear()
asMock(storeState.setActiveView).mockClear()
asMock(storeState.setActiveWorktree).mockClear()
asMock(storeState.setActiveTabType).mockClear()
asMock(storeState.setActiveTab).mockClear()
// Regression: a workspace created in the background spawns its agent terminal
// here; the tab must still be adopted, but the sidebar must not scroll to it.
harness.createTerminal({
worktreeId: 'wt-2',
ptyId: 'pty-background-create',
activate: false,
surfaceOwner: false,
tabId: 'tab-background-create'
})
expect(storeState.createTab).toHaveBeenCalledWith('wt-2', undefined, undefined, {
initialPtyId: 'pty-background-create',
activate: false,
id: 'tab-background-create'
})
expect(storeState.revealWorktreeInSidebar).not.toHaveBeenCalled()
expect(storeState.setActiveView).not.toHaveBeenCalled()
expect(storeState.setActiveWorktree).not.toHaveBeenCalled()
expect(storeState.setActiveTabType).not.toHaveBeenCalled()
expect(storeState.setActiveTab).not.toHaveBeenCalled()
})
it('replies to a requested terminal create without scrolling the sidebar when surfaceOwner is false', async () => {
const storeState = createBackgroundWorkspaceState()
const harness = await loadIpcEventsHarness(storeState)
harness.useIpcEvents()
// Control: the same request without surfaceOwner still reveals its workspace.
harness.requestTerminalCreate({
requestId: 'req-cli',
worktreeId: 'wt-2',
title: 'Claude'
})
expect(harness.replyTerminalCreate).toHaveBeenCalledWith({
requestId: 'req-cli',
tabId: 'tab-minted',
title: 'Claude'
})
expect(storeState.revealWorktreeInSidebar).toHaveBeenCalledWith('wt-2')
asMock(storeState.createTab).mockClear()
asMock(storeState.revealWorktreeInSidebar).mockClear()
asMock(storeState.setActiveView).mockClear()
asMock(storeState.setActiveWorktree).mockClear()
asMock(storeState.setActiveTabType).mockClear()
asMock(storeState.setActiveTab).mockClear()
harness.replyTerminalCreate.mockClear()
harness.requestTerminalCreate({
requestId: 'req-background-create',
worktreeId: 'wt-2',
title: 'Claude',
surfaceOwner: false
})
expect(storeState.createTab).toHaveBeenCalledWith('wt-2', undefined, undefined, {
activate: false,
recordInteraction: false
})
expect(harness.replyTerminalCreate).toHaveBeenCalledWith({
requestId: 'req-background-create',
tabId: 'tab-minted',
title: 'Claude'
})
expect(storeState.revealWorktreeInSidebar).not.toHaveBeenCalled()
expect(storeState.setActiveView).not.toHaveBeenCalled()
expect(storeState.setActiveWorktree).not.toHaveBeenCalled()
expect(storeState.setActiveTabType).not.toHaveBeenCalled()
expect(storeState.setActiveTab).not.toHaveBeenCalled()
})
})
+5 -2
View File
@@ -1416,6 +1416,7 @@ export function useIpcEvents(): void {
activate,
focus,
presentation,
surfaceOwner,
tabId,
leafId,
splitFromLeafId,
@@ -1430,7 +1431,8 @@ export function useIpcEvents(): void {
focus
})
const shouldActivate = terminalPresentation === 'focused'
const shouldSurfaceOwner = terminalPresentation !== 'background'
const shouldSurfaceOwner =
terminalPresentation !== 'background' && surfaceOwner !== false
if (shouldActivate) {
activateTerminalInitiatedWorktree(store, worktreeId)
}
@@ -1682,7 +1684,8 @@ export function useIpcEvents(): void {
}
const terminalPresentation = resolveTerminalPresentation(data)
const shouldActivate = terminalPresentation === 'focused'
const shouldSurfaceOwner = terminalPresentation !== 'background'
const shouldSurfaceOwner =
terminalPresentation !== 'background' && data.surfaceOwner !== false
if (shouldActivate) {
activateTerminalInitiatedWorktree(store, worktreeId)
}
+6
View File
@@ -622,6 +622,12 @@ type RuntimeTerminalCreateBaseRequestPayload = {
title?: string
activate?: boolean
presentation?: RuntimeTerminalPresentation
/**
* Why: adopting a terminal is separate from pointing the user at it. `false`
* keeps the tab silent — no sidebar reveal, no tab focus — for terminals the
* user never asked to see (e.g. a workspace created in the background).
*/
surfaceOwner?: boolean
}
export type RuntimeTerminalCreateRequestPayload =