mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
fix(sidebar): stop background workspace creation from scrolling the sidebar (#11530)
* 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. * fix(sidebar): keep split-mode setup panes silent, tighten surfaceOwner Review catch: with setupScriptLaunchMode split-vertical/horizontal the Setup terminal goes through splitTerminal, whose reveal payload had no surfaceOwner, so a background create still scrolled the sidebar in that configuration. Also narrow surfaceOwner to `false` so "surface it" can only be expressed by omitting the key, and fold the repeated conditional spreads into ownerSurfacing.
This commit is contained in:
@@ -35666,10 +35666,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
|
||||
})
|
||||
@@ -35900,15 +35903,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,
|
||||
@@ -35997,6 +36179,172 @@ describe('OrcaRuntimeService', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('does not surface the new workspace when a background create splits its setup pane', async () => {
|
||||
const runtimeStore = {
|
||||
...store,
|
||||
getSettings: () => ({
|
||||
...store.getSettings(),
|
||||
setupScriptLaunchMode: 'split-horizontal' as const
|
||||
})
|
||||
}
|
||||
const runtime = new OrcaRuntimeService(runtimeStore as never)
|
||||
const activateWorktree = vi.fn()
|
||||
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-bg-setup-split' })
|
||||
const spawn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 'pty-bg-split-main' })
|
||||
.mockResolvedValueOnce({ id: 'pty-bg-split-setup' })
|
||||
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-bg-split-setup')
|
||||
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-bg-split-setup')
|
||||
vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm install' } })
|
||||
vi.mocked(shouldRunSetupForCreate).mockReturnValue(true)
|
||||
vi.mocked(createSetupRunnerScript).mockReturnValue({
|
||||
runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh',
|
||||
envVars: {
|
||||
ORCA_ROOT_PATH: '/tmp/repo',
|
||||
ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-bg-split-setup'
|
||||
}
|
||||
})
|
||||
vi.mocked(listWorktrees).mockResolvedValue([
|
||||
{
|
||||
path: '/tmp/workspaces/runtime-bg-split-setup',
|
||||
head: 'def',
|
||||
branch: 'runtime-bg-split-setup',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
])
|
||||
|
||||
const result = await runtime.createManagedWorktree({
|
||||
repoSelector: 'id:repo-1',
|
||||
name: 'runtime-bg-split-setup',
|
||||
setupDecision: 'run'
|
||||
})
|
||||
|
||||
// Split launch modes reach the renderer through splitTerminal, not
|
||||
// createTerminal — that path must suppress owner surfacing too.
|
||||
expect(activateWorktree).not.toHaveBeenCalled()
|
||||
await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2))
|
||||
const mainEnv = (spawn.mock.calls[0]![0] as { env?: Record<string, string> }).env ?? {}
|
||||
await vi.waitFor(() =>
|
||||
expect(revealTerminalSession).toHaveBeenLastCalledWith(
|
||||
result.worktree.id,
|
||||
expect.objectContaining({
|
||||
ptyId: 'pty-bg-split-setup',
|
||||
tabId: mainEnv.ORCA_TAB_ID,
|
||||
activate: false,
|
||||
surfaceOwner: false,
|
||||
splitDirection: 'horizontal'
|
||||
})
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('still surfaces the new workspace when an activating create splits its setup pane', async () => {
|
||||
const runtimeStore = {
|
||||
...store,
|
||||
getSettings: () => ({
|
||||
...store.getSettings(),
|
||||
setupScriptLaunchMode: 'split-horizontal' as const
|
||||
})
|
||||
}
|
||||
const runtime = new OrcaRuntimeService(runtimeStore as never)
|
||||
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-active-setup-split' })
|
||||
const spawn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 'pty-active-split-main' })
|
||||
.mockResolvedValueOnce({ id: 'pty-active-split-setup' })
|
||||
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-active-split-setup')
|
||||
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-active-split-setup')
|
||||
vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm install' } })
|
||||
vi.mocked(shouldRunSetupForCreate).mockReturnValue(true)
|
||||
vi.mocked(createSetupRunnerScript).mockReturnValue({
|
||||
runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh',
|
||||
envVars: {
|
||||
ORCA_ROOT_PATH: '/tmp/repo',
|
||||
ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-active-split-setup'
|
||||
}
|
||||
})
|
||||
vi.mocked(listWorktrees).mockResolvedValue([
|
||||
{
|
||||
path: '/tmp/workspaces/runtime-active-split-setup',
|
||||
head: 'def',
|
||||
branch: 'runtime-active-split-setup',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
])
|
||||
|
||||
const result = await runtime.createManagedWorktree({
|
||||
repoSelector: 'id:repo-1',
|
||||
name: 'runtime-active-split-setup',
|
||||
activate: true,
|
||||
setupDecision: 'run',
|
||||
createdWithAgent: 'claude',
|
||||
startup: { command: 'claude' }
|
||||
})
|
||||
|
||||
// No-regression guard: an explicit --activate still reveals the workspace.
|
||||
await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2))
|
||||
await vi.waitFor(() =>
|
||||
expect(revealTerminalSession).toHaveBeenLastCalledWith(
|
||||
result.worktree.id,
|
||||
expect.objectContaining({
|
||||
ptyId: 'pty-active-split-setup',
|
||||
splitDirection: 'horizontal'
|
||||
})
|
||||
)
|
||||
)
|
||||
const splitRevealPayload = revealTerminalSession.mock.lastCall?.[1] as
|
||||
| { surfaceOwner?: boolean }
|
||||
| undefined
|
||||
expect(splitRevealPayload).not.toHaveProperty('surfaceOwner')
|
||||
})
|
||||
|
||||
it('does not warn when setup is explicitly skipped for CLI-created worktrees', async () => {
|
||||
const metaById: Record<string, WorktreeMeta> = {}
|
||||
const runtimeStore = {
|
||||
|
||||
@@ -1248,6 +1248,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?: false
|
||||
tabId?: string
|
||||
leafId?: string
|
||||
sessionId?: string
|
||||
@@ -1603,6 +1607,12 @@ function createTerminalRevealWarning(handle: string, error?: unknown): string {
|
||||
].join(' ')
|
||||
}
|
||||
|
||||
// Why: an absent `surfaceOwner` means "default", so surfacing callers must omit
|
||||
// the key rather than send `true`.
|
||||
function ownerSurfacing(shouldSurface: boolean): { surfaceOwner?: false } {
|
||||
return shouldSurface ? {} : { surfaceOwner: false }
|
||||
}
|
||||
|
||||
function resolveTerminalPresentation(opts: {
|
||||
presentation?: RuntimeTerminalPresentation
|
||||
focus?: boolean
|
||||
@@ -1651,6 +1661,7 @@ type RuntimeNotifier = {
|
||||
viewMode?: 'terminal' | 'chat'
|
||||
activate?: boolean
|
||||
presentation?: RuntimeTerminalPresentation
|
||||
surfaceOwner?: false
|
||||
tabId?: string
|
||||
leafId?: string
|
||||
splitFromLeafId?: string
|
||||
@@ -19674,7 +19685,8 @@ export class OrcaRuntimeService {
|
||||
private async createDefaultTabTerminals(
|
||||
worktreeSelector: string,
|
||||
worktreeId: string,
|
||||
defaultTabs: CreateWorktreeResult['defaultTabs'] | undefined
|
||||
defaultTabs: CreateWorktreeResult['defaultTabs'] | undefined,
|
||||
surfacing: { surfaceOwner?: false } = {}
|
||||
): Promise<string[]> {
|
||||
if (!defaultTabs || defaultTabs.tabs.length === 0 || !this.ptyController?.spawn) {
|
||||
return []
|
||||
@@ -19685,7 +19697,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 } : {}),
|
||||
...surfacing
|
||||
})
|
||||
handles.push(terminal.handle)
|
||||
if (template.color && terminal.tabId) {
|
||||
@@ -19716,17 +19729,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?: false
|
||||
}): Promise<{ setupSpawned: boolean; setupTerminalHandle: string | null }> {
|
||||
if (!this.ptyController?.spawn) {
|
||||
return { setupSpawned: false, setupTerminalHandle: null }
|
||||
}
|
||||
const surfacing = ownerSurfacing(args.surfaceOwner !== false)
|
||||
let setupSpawned = false
|
||||
let setupTerminalHandle: string | null = null
|
||||
try {
|
||||
const defaultTabHandles = await this.createDefaultTabTerminals(
|
||||
args.worktreeSelector,
|
||||
args.worktreeId,
|
||||
args.defaultTabs
|
||||
args.defaultTabs,
|
||||
surfacing
|
||||
)
|
||||
let primaryTerminalHandle = args.primaryTerminalHandle ?? defaultTabHandles[0] ?? null
|
||||
const setupLaunchMode =
|
||||
@@ -19736,7 +19754,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, surfacing)
|
||||
primaryTerminalHandle = terminal.handle
|
||||
}
|
||||
if (args.setup) {
|
||||
@@ -19762,12 +19780,14 @@ export class OrcaRuntimeService {
|
||||
direction: setupLaunchMode === 'split-horizontal' ? 'horizontal' : 'vertical',
|
||||
command: setupCommand,
|
||||
env: setupEnv,
|
||||
activate: false
|
||||
activate: false,
|
||||
...surfacing
|
||||
})
|
||||
: this.createTerminal(args.worktreeSelector, {
|
||||
title: 'Setup',
|
||||
command: setupCommand,
|
||||
env: setupEnv
|
||||
env: setupEnv,
|
||||
...surfacing
|
||||
}))
|
||||
setupTerminalHandle = setupTerminal.handle
|
||||
setupSpawned = true
|
||||
@@ -20039,7 +20059,8 @@ export class OrcaRuntimeService {
|
||||
: {}),
|
||||
...(effectiveCreatedWithAgent ? { launchAgent: effectiveCreatedWithAgent } : {}),
|
||||
startupCommandDelivery: effectiveStartup.startupCommandDelivery,
|
||||
telemetry: effectiveStartup.telemetry
|
||||
telemetry: effectiveStartup.telemetry,
|
||||
...ownerSurfacing(shouldActivate)
|
||||
})
|
||||
if (effectiveDraftPaste) {
|
||||
this.pasteStartupDraftWhenReady(terminal.handle, effectiveDraftPaste)
|
||||
@@ -20070,7 +20091,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
|
||||
@@ -20774,7 +20795,8 @@ export class OrcaRuntimeService {
|
||||
...(sequencedStartup.launchConfig ? { launchConfig: sequencedStartup.launchConfig } : {}),
|
||||
...(effectiveCreatedWithAgent ? { launchAgent: effectiveCreatedWithAgent } : {}),
|
||||
startupCommandDelivery: sequencedStartup.startupCommandDelivery,
|
||||
telemetry: sequencedStartup.telemetry
|
||||
telemetry: sequencedStartup.telemetry,
|
||||
...ownerSurfacing(shouldActivate)
|
||||
})
|
||||
if (effectiveDraftPaste) {
|
||||
this.pasteStartupDraftWhenReady(terminal.handle, effectiveDraftPaste)
|
||||
@@ -20875,7 +20897,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.
|
||||
@@ -20891,7 +20914,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
|
||||
@@ -21069,6 +21092,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
|
||||
@@ -21116,7 +21140,8 @@ export class OrcaRuntimeService {
|
||||
...(sequencedStartup.launchConfig ? { launchConfig: sequencedStartup.launchConfig } : {}),
|
||||
...(args.createdWithAgent ? { launchAgent: args.createdWithAgent } : {}),
|
||||
startupCommandDelivery: sequencedStartup.startupCommandDelivery,
|
||||
telemetry: sequencedStartup.telemetry
|
||||
telemetry: sequencedStartup.telemetry,
|
||||
...ownerSurfacing(shouldActivate)
|
||||
})
|
||||
if (args.startupDraftPaste) {
|
||||
this.pasteStartupDraftWhenReady(terminal.handle, args.startupDraftPaste)
|
||||
@@ -21137,7 +21162,6 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
}
|
||||
|
||||
const shouldActivate = args.activate === true || args.runHooks === true
|
||||
if (shouldActivate) {
|
||||
const runtimeWillProvisionTerminals =
|
||||
didSpawnStartup && Boolean(result.setup || result.defaultTabs)
|
||||
@@ -21219,7 +21243,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.
|
||||
@@ -21235,7 +21260,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
|
||||
@@ -23647,6 +23672,7 @@ export class OrcaRuntimeService {
|
||||
...(launchOpts.viewMode ? { viewMode: launchOpts.viewMode } : {}),
|
||||
activate: presentation === 'focused',
|
||||
...(presentation ? { presentation } : {}),
|
||||
...ownerSurfacing(opts.surfaceOwner !== false),
|
||||
tabId,
|
||||
leafId
|
||||
})
|
||||
@@ -23731,7 +23757,8 @@ export class OrcaRuntimeService {
|
||||
startupCommandDelivery: launchOpts.startupCommandDelivery,
|
||||
title: launchOpts.title,
|
||||
activate: presentation === 'focused',
|
||||
...(presentation ? { presentation } : {})
|
||||
...(presentation ? { presentation } : {}),
|
||||
...ownerSurfacing(opts.surfaceOwner !== false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24871,6 +24898,9 @@ export class OrcaRuntimeService {
|
||||
env?: Record<string, string>
|
||||
envToDelete?: string[]
|
||||
activate?: boolean
|
||||
// Why: same split as createTerminal — adopt the pane without revealing its
|
||||
// workspace, for splits the user never asked to see.
|
||||
surfaceOwner?: false
|
||||
telemetrySource?: TerminalPaneSplitSource
|
||||
} = {}
|
||||
): Promise<RuntimeTerminalSplit> {
|
||||
@@ -24908,6 +24938,9 @@ export class OrcaRuntimeService {
|
||||
env?: Record<string, string>
|
||||
envToDelete?: string[]
|
||||
activate?: boolean
|
||||
// Why: same split as createTerminal — adopt the pane without revealing its
|
||||
// workspace, for splits the user never asked to see.
|
||||
surfaceOwner?: false
|
||||
telemetrySource?: TerminalPaneSplitSource
|
||||
} = {}
|
||||
): Promise<RuntimeTerminalSplit> {
|
||||
@@ -24958,6 +24991,7 @@ export class OrcaRuntimeService {
|
||||
ptyId: result.id,
|
||||
title: null,
|
||||
activate: opts.activate !== false,
|
||||
...ownerSurfacing(opts.surfaceOwner !== false),
|
||||
tabId: parentTabId,
|
||||
leafId,
|
||||
splitFromLeafId: parsedPaneKey.leafId,
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -3140,6 +3140,7 @@ export type PreloadApi = {
|
||||
activate?: boolean
|
||||
focus?: boolean
|
||||
presentation?: RuntimeTerminalPresentation
|
||||
surfaceOwner?: false
|
||||
tabId?: string
|
||||
leafId?: string
|
||||
splitFromLeafId?: string
|
||||
|
||||
@@ -3701,6 +3701,7 @@ const api = {
|
||||
activate?: boolean
|
||||
focus?: boolean
|
||||
presentation?: RuntimeTerminalPresentation
|
||||
surfaceOwner?: false
|
||||
tabId?: string
|
||||
leafId?: string
|
||||
splitFromLeafId?: string
|
||||
@@ -3726,6 +3727,7 @@ const api = {
|
||||
activate?: boolean
|
||||
focus?: boolean
|
||||
presentation?: RuntimeTerminalPresentation
|
||||
surfaceOwner?: false
|
||||
tabId?: string
|
||||
leafId?: string
|
||||
splitFromLeafId?: string
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { vi } from 'vitest'
|
||||
import type * as ReactModule from 'react'
|
||||
import type { TerminalPaneLayoutNode } from '../../../shared/types'
|
||||
|
||||
export type CreateTerminalRequest = {
|
||||
requestId?: string
|
||||
worktreeId: string
|
||||
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 = {
|
||||
tabsByWorktree: Record<string, HarnessTab[]>
|
||||
ptyIdsByTabId: Record<string, string[]>
|
||||
terminalLayoutsByTabId: Record<
|
||||
string,
|
||||
{ root?: TerminalPaneLayoutNode; ptyIdsByLeafId?: Record<string, string> }
|
||||
>
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** Store surface useIpcEvents touches at mount plus the terminal-reveal path. */
|
||||
export function createHarnessStoreState(
|
||||
overrides: Partial<HarnessStoreState> & Pick<HarnessStoreState, 'tabsByWorktree'>
|
||||
): HarnessStoreState {
|
||||
const state: HarnessStoreState = {
|
||||
createTab: vi.fn(() => ({ id: 'tab-minted' })),
|
||||
setActiveView: vi.fn(),
|
||||
setActiveWorktree: vi.fn(),
|
||||
markWorktreeVisited: vi.fn(),
|
||||
recordWorktreeVisit: vi.fn(),
|
||||
isNavigatingHistory: false,
|
||||
setActiveTabType: vi.fn(),
|
||||
setActiveTab: vi.fn(),
|
||||
revealWorktreeInSidebar: vi.fn(),
|
||||
setTabCustomTitle: vi.fn(),
|
||||
queueTabStartupCommand: vi.fn(),
|
||||
registerAgentLaunchConfig: vi.fn(),
|
||||
clearAgentLaunchConfig: vi.fn(),
|
||||
updateTabPtyId: vi.fn(),
|
||||
setTabLayout: vi.fn(),
|
||||
setTabBarOrder: vi.fn(),
|
||||
clearTabPtyId: vi.fn(),
|
||||
setUpdateStatus: vi.fn(),
|
||||
fetchRepos: vi.fn(),
|
||||
fetchWorktrees: vi.fn(),
|
||||
closeModal: vi.fn(),
|
||||
openModal: vi.fn(),
|
||||
setActiveRepo: vi.fn(),
|
||||
setIsFullScreen: vi.fn(),
|
||||
updateBrowserPageState: vi.fn(),
|
||||
setEditorFontZoomLevel: vi.fn(),
|
||||
setRateLimitsFromPush: vi.fn(),
|
||||
setSshConnectionState: vi.fn(),
|
||||
setSshTargetLabels: vi.fn(),
|
||||
setPortForwards: vi.fn(),
|
||||
clearPortForwards: vi.fn(),
|
||||
setDetectedPorts: vi.fn(),
|
||||
enqueueSshCredentialRequest: vi.fn(),
|
||||
removeSshCredentialRequest: vi.fn(),
|
||||
ptyIdsByTabId: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
folderWorkspaces: [],
|
||||
projectGroups: [],
|
||||
repos: [{ id: 'repo-1', connectionId: null, executionHostId: 'local' }],
|
||||
worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] },
|
||||
openFiles: [],
|
||||
browserTabsByWorktree: {},
|
||||
tabBarOrderByWorktree: {},
|
||||
activeModal: null,
|
||||
activeWorktreeId: 'wt-1',
|
||||
activeView: 'terminal',
|
||||
activeTabType: 'terminal',
|
||||
editorFontZoomLevel: 0,
|
||||
settings: {
|
||||
terminalFontSize: 13,
|
||||
experimentalNativeChat: false,
|
||||
openAgentTabsInChatByDefault: false,
|
||||
activeRuntimeEnvironmentId: undefined
|
||||
},
|
||||
...overrides
|
||||
}
|
||||
if (overrides.setTabLayout === undefined) {
|
||||
state.setTabLayout = vi.fn(
|
||||
(tabId: string, layout: HarnessStoreState['terminalLayoutsByTabId'][string]) => {
|
||||
state.terminalLayoutsByTabId[tabId] = layout
|
||||
}
|
||||
)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
/** Subscription no-ops for every listener useIpcEvents attaches beyond the ones under test. */
|
||||
function createApiNamespaceStub(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return new Proxy(overrides, {
|
||||
get: (target, prop: string) => (prop in target ? target[prop] : () => () => {})
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads useIpcEvents against a stubbed preload API and returns a driver for the
|
||||
* create-terminal IPC, so reveal/adoption behavior is asserted through the hook.
|
||||
*/
|
||||
export async function loadIpcEventsHarness(
|
||||
storeState: HarnessStoreState
|
||||
): Promise<IpcEventsHarness> {
|
||||
const replyTerminalCreate = vi.fn()
|
||||
let createTerminalListener: ((request: CreateTerminalRequest) => void) | null = null
|
||||
let requestTerminalCreateListener: ((request: RequestTerminalCreateRequest) => void) | null = null
|
||||
|
||||
vi.resetModules()
|
||||
vi.unstubAllGlobals()
|
||||
|
||||
vi.doMock('react', async () => {
|
||||
const actual = await vi.importActual<typeof ReactModule>('react')
|
||||
return { ...actual, useEffect: (effect: () => void | (() => void)) => void effect() }
|
||||
})
|
||||
vi.doMock('../store', () => ({
|
||||
useAppStore: { subscribe: vi.fn(() => () => {}), getState: () => storeState }
|
||||
}))
|
||||
vi.doMock('@/lib/ui-zoom', () => ({ applyUIZoom: vi.fn() }))
|
||||
vi.doMock('@/lib/worktree-activation', () => ({
|
||||
activateAndRevealWorktree: vi.fn(),
|
||||
ensureWorktreeHasInitialTerminal: vi.fn()
|
||||
}))
|
||||
vi.doMock('@/components/sidebar/visible-worktrees', () => ({ getVisibleWorktreeIds: () => [] }))
|
||||
vi.doMock('@/lib/floating-workspace-terminal-actions', () => ({
|
||||
createFloatingWorkspaceTerminalTab: vi.fn(),
|
||||
isEmptyFloatingWorkspacePanelVisible: () => false,
|
||||
isFloatingWorkspacePanelFocused: () => false
|
||||
}))
|
||||
vi.doMock('@/runtime/web-runtime-session', () => ({
|
||||
activateWebRuntimeSessionTab: vi.fn(),
|
||||
closeWebRuntimeSessionTab: vi.fn(),
|
||||
createWebRuntimeSessionBrowserTab: vi.fn().mockResolvedValue(false),
|
||||
createWebRuntimeSessionTerminal: vi.fn().mockResolvedValue({ status: 'failed', message: '' }),
|
||||
isWebRuntimeSessionActive: vi.fn(() => false)
|
||||
}))
|
||||
vi.doMock('@/lib/focus-terminal-tab-surface', () => ({ focusTerminalTabSurface: vi.fn() }))
|
||||
vi.doMock('@/runtime/sync-runtime-graph', () => ({
|
||||
focusRuntimeTerminalSurface: vi.fn(() => false)
|
||||
}))
|
||||
vi.doMock('@/lib/activate-tab-and-focus-pane', () => ({ activateTabAndFocusPane: vi.fn() }))
|
||||
|
||||
vi.stubGlobal('window', {
|
||||
dispatchEvent: vi.fn(),
|
||||
api: new Proxy(
|
||||
{
|
||||
ui: createApiNamespaceStub({
|
||||
getZoomLevel: () => 0,
|
||||
consumePendingOpenSettings: () => Promise.resolve(false),
|
||||
set: vi.fn(),
|
||||
replyTabCreate: vi.fn(),
|
||||
replyTabClose: vi.fn(),
|
||||
replyTabSetProfile: vi.fn(),
|
||||
replyTerminalCreate,
|
||||
onCreateTerminal: (listener: (request: CreateTerminalRequest) => void) => {
|
||||
createTerminalListener = listener
|
||||
return () => {}
|
||||
},
|
||||
onRequestTerminalCreate: (listener: (request: RequestTerminalCreateRequest) => void) => {
|
||||
requestTerminalCreateListener = listener
|
||||
return () => {}
|
||||
}
|
||||
}),
|
||||
rateLimits: {
|
||||
get: () => Promise.resolve({ limits: {}, lastUpdatedAt: 0 }),
|
||||
onUpdate: () => () => {}
|
||||
},
|
||||
runtime: {
|
||||
getTerminalFitOverrides: () => Promise.resolve([]),
|
||||
getTerminalDrivers: () => Promise.resolve([]),
|
||||
getBrowserDrivers: () => Promise.resolve([]),
|
||||
onTerminalFitOverrideChanged: () => () => {},
|
||||
onTerminalDriverChanged: () => () => {},
|
||||
onBrowserDriverChanged: () => () => {}
|
||||
},
|
||||
ssh: {
|
||||
listTargets: () => Promise.resolve([]),
|
||||
listPortForwards: () => Promise.resolve([]),
|
||||
listDetectedPorts: () => Promise.resolve([]),
|
||||
listRemovedTargetLabels: () => Promise.resolve([]),
|
||||
getState: () => Promise.resolve(null),
|
||||
onStateChanged: () => () => {},
|
||||
onCredentialRequest: () => () => {},
|
||||
onCredentialResolved: () => () => {},
|
||||
onPortForwardsChanged: () => () => {},
|
||||
onDetectedPortsChanged: () => () => {}
|
||||
},
|
||||
updater: {
|
||||
getStatus: () => Promise.resolve({ state: 'idle' }),
|
||||
onStatus: () => () => {},
|
||||
onClearDismissal: () => () => {}
|
||||
},
|
||||
mobile: createApiNamespaceStub({
|
||||
consumePendingUnpairedDeviceAuthFailure: () => Promise.resolve(false)
|
||||
}),
|
||||
remoteWorkspace: createApiNamespaceStub({ clientId: () => Promise.resolve(null) })
|
||||
} as Record<string, unknown>,
|
||||
{ get: (target, prop: string) => target[prop] ?? createApiNamespaceStub() }
|
||||
)
|
||||
})
|
||||
|
||||
const { useIpcEvents } = await import('./useIpcEvents')
|
||||
return {
|
||||
useIpcEvents,
|
||||
createTerminal: (request) => {
|
||||
if (typeof createTerminalListener !== 'function') {
|
||||
throw new Error('Expected the create-terminal listener to be registered')
|
||||
}
|
||||
createTerminalListener(request)
|
||||
},
|
||||
requestTerminalCreate: (request) => {
|
||||
if (typeof requestTerminalCreateListener !== 'function') {
|
||||
throw new Error('Expected the request-terminal-create listener to be registered')
|
||||
}
|
||||
requestTerminalCreateListener(request)
|
||||
},
|
||||
replyTerminalCreate
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1406,6 +1406,7 @@ export function useIpcEvents(): void {
|
||||
activate,
|
||||
focus,
|
||||
presentation,
|
||||
surfaceOwner,
|
||||
tabId,
|
||||
leafId,
|
||||
splitFromLeafId,
|
||||
@@ -1420,7 +1421,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)
|
||||
}
|
||||
@@ -1676,7 +1678,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)
|
||||
}
|
||||
|
||||
@@ -622,6 +622,13 @@ 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).
|
||||
* Absent means "surface it", so this is a suppression switch, never `true`.
|
||||
*/
|
||||
surfaceOwner?: false
|
||||
}
|
||||
|
||||
export type RuntimeTerminalCreateRequestPayload =
|
||||
|
||||
Reference in New Issue
Block a user