mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix(runtime): isolate paired terminal creates from host focus (#17713)
This commit is contained in:
@@ -465,6 +465,43 @@ describe('paired runtime navigation isolation', () => {
|
||||
).toBe('activateWorktree')
|
||||
})
|
||||
|
||||
it('normalizes a paired focused terminal.create before host-renderer activation', async () => {
|
||||
const harness = await startHarness()
|
||||
const created = { handle: 'term-b', worktreeId: CLIENT_B_WORKTREE_ID, title: null }
|
||||
const createTerminal = vi
|
||||
.spyOn(harness.runtime, 'createTerminal')
|
||||
.mockImplementation(async (_worktree, options) => {
|
||||
if (options?.presentation === 'focused') {
|
||||
harness.hostSelections.worktreeId = CLIENT_B_WORKTREE_ID
|
||||
}
|
||||
return created as never
|
||||
})
|
||||
vi.spyOn(harness.runtime, 'dedupeTerminalCreate').mockImplementation(
|
||||
async (_owner, worktree, _mutationId, _reconcile, run) => run(worktree, undefined)
|
||||
)
|
||||
|
||||
send(harness.clientB, {
|
||||
id: 'terminal-create-b',
|
||||
method: 'terminal.create',
|
||||
params: {
|
||||
worktree: `id:${CLIENT_B_WORKTREE_ID}`,
|
||||
presentation: 'focused'
|
||||
}
|
||||
})
|
||||
await expect(harness.readerB.next('terminal-create-b')).resolves.toMatchObject({
|
||||
ok: true,
|
||||
result: { terminal: created }
|
||||
})
|
||||
expect(createTerminal).toHaveBeenCalledWith(
|
||||
`id:${CLIENT_B_WORKTREE_ID}`,
|
||||
expect.objectContaining({ presentation: 'background', focus: false, activate: false })
|
||||
)
|
||||
expect(harness.hostSelections).toEqual({
|
||||
worktreeId: HOST_WORKTREE_ID,
|
||||
tabId: 'host-tab'
|
||||
})
|
||||
})
|
||||
|
||||
it('still reveals a host-originated create-with-activate on the host and every client', async () => {
|
||||
const harness = await startHarness()
|
||||
await subscribeBothClientEventStreams(harness)
|
||||
|
||||
@@ -60,4 +60,76 @@ describe('terminal.create RPC idempotency', () => {
|
||||
)
|
||||
expect(result).toEqual({ terminal })
|
||||
})
|
||||
|
||||
it('does not let a paired focused create navigate the host by default', async () => {
|
||||
const terminal = { handle: 'terminal-focused', worktreeId: 'worktree-1', title: null }
|
||||
const createTerminal = vi.fn(async () => terminal)
|
||||
const dedupeTerminalCreate = vi.fn(
|
||||
async (
|
||||
_clientIdentity: string,
|
||||
_worktree: string | undefined,
|
||||
_mutationId: string | undefined,
|
||||
_reconcileExisting: boolean,
|
||||
run: (worktree: string | undefined, handle: string | undefined) => Promise<typeof terminal>
|
||||
) => run('id:worktree-1', undefined)
|
||||
)
|
||||
const method = TERMINAL_METHODS.find((candidate) => candidate.name === 'terminal.create')
|
||||
if (!method) {
|
||||
throw new Error('terminal.create method missing')
|
||||
}
|
||||
|
||||
await method.handler(
|
||||
{
|
||||
worktree: 'id:worktree-1',
|
||||
presentation: 'focused',
|
||||
focus: true,
|
||||
activate: true
|
||||
},
|
||||
{
|
||||
runtime: { createTerminal, dedupeTerminalCreate },
|
||||
pairedDeviceId: 'device-b',
|
||||
clientKind: 'runtime'
|
||||
} as unknown as RpcContext,
|
||||
vi.fn()
|
||||
)
|
||||
|
||||
expect(createTerminal).toHaveBeenCalledWith(
|
||||
'id:worktree-1',
|
||||
expect.objectContaining({
|
||||
presentation: 'background',
|
||||
focus: false,
|
||||
activate: false
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves focus for an in-process caller', async () => {
|
||||
const createTerminal = vi.fn(async () => ({ handle: 'terminal-host' }))
|
||||
const dedupeTerminalCreate = vi.fn(
|
||||
async (
|
||||
_owner: string,
|
||||
_worktree: string | undefined,
|
||||
_mutationId: string | undefined,
|
||||
_reconcile: boolean,
|
||||
run: (worktree: string | undefined, handle: string | undefined) => Promise<unknown>
|
||||
) => run('id:worktree-1', undefined)
|
||||
)
|
||||
const method = TERMINAL_METHODS.find((candidate) => candidate.name === 'terminal.create')
|
||||
if (!method) {
|
||||
throw new Error('terminal.create method missing')
|
||||
}
|
||||
|
||||
await method.handler(
|
||||
{ worktree: 'id:worktree-1', presentation: 'focused', focus: true, activate: true },
|
||||
{
|
||||
runtime: { createTerminal, dedupeTerminalCreate }
|
||||
} as unknown as RpcContext,
|
||||
vi.fn()
|
||||
)
|
||||
|
||||
expect(createTerminal).toHaveBeenCalledWith(
|
||||
'id:worktree-1',
|
||||
expect.objectContaining({ presentation: 'focused', focus: true, activate: true })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,38 +33,49 @@ export const TERMINAL_LIFECYCLE_METHODS: RpcAnyMethod[] = [
|
||||
defineMethod({
|
||||
name: 'terminal.create',
|
||||
params: TerminalCreateParams,
|
||||
handler: async (params, { runtime, pairedDeviceId, clientId }) => ({
|
||||
terminal: await runtime.dedupeTerminalCreate(
|
||||
pairedDeviceId ?? clientId ?? 'local',
|
||||
params.worktree,
|
||||
params.clientMutationId,
|
||||
params.reconcileExisting === true,
|
||||
(canonicalWorktreeSelector, preAllocatedHandle) =>
|
||||
runtime.createTerminal(canonicalWorktreeSelector, {
|
||||
command: params.command,
|
||||
startupCommandDelivery: params.startupCommandDelivery,
|
||||
env: params.env,
|
||||
envToDelete: params.envToDelete,
|
||||
...(params.launchConfig ? { launchConfig: params.launchConfig } : {}),
|
||||
...(params.resumeProviderSession
|
||||
? { resumeProviderSession: params.resumeProviderSession }
|
||||
: {}),
|
||||
...(params.launchToken ? { launchToken: params.launchToken } : {}),
|
||||
...(params.launchAgent ? { launchAgent: params.launchAgent } : {}),
|
||||
...(params.terminalColorQueryReplies
|
||||
? { terminalColorQueryReplies: params.terminalColorQueryReplies }
|
||||
: {}),
|
||||
title: params.title,
|
||||
focus: params.focus === true,
|
||||
rendererBacked: params.rendererBacked === true,
|
||||
activate: params.activate === true,
|
||||
presentation: params.presentation,
|
||||
tabId: params.tabId,
|
||||
leafId: params.leafId,
|
||||
...(preAllocatedHandle ? { preAllocatedHandle } : {})
|
||||
})
|
||||
)
|
||||
})
|
||||
handler: async (params, { runtime, pairedDeviceId, clientId, clientKind }) => {
|
||||
// A focused terminal create predates paired-client navigation. Keep the
|
||||
// authority boundary here so a remote caller cannot activate the host
|
||||
// renderer. This legacy RPC remains a background create for paired viewers;
|
||||
// caller-local selection belongs to the session-tab RPC flow.
|
||||
const pairedViewer = clientKind !== undefined
|
||||
const focus = pairedViewer ? false : params.focus === true
|
||||
const activate = pairedViewer ? false : params.activate === true
|
||||
const presentation =
|
||||
pairedViewer && params.presentation === 'focused' ? 'background' : params.presentation
|
||||
return {
|
||||
terminal: await runtime.dedupeTerminalCreate(
|
||||
pairedDeviceId ?? clientId ?? 'local',
|
||||
params.worktree,
|
||||
params.clientMutationId,
|
||||
params.reconcileExisting === true,
|
||||
(canonicalWorktreeSelector, preAllocatedHandle) =>
|
||||
runtime.createTerminal(canonicalWorktreeSelector, {
|
||||
command: params.command,
|
||||
startupCommandDelivery: params.startupCommandDelivery,
|
||||
env: params.env,
|
||||
envToDelete: params.envToDelete,
|
||||
...(params.launchConfig ? { launchConfig: params.launchConfig } : {}),
|
||||
...(params.resumeProviderSession
|
||||
? { resumeProviderSession: params.resumeProviderSession }
|
||||
: {}),
|
||||
...(params.launchToken ? { launchToken: params.launchToken } : {}),
|
||||
...(params.launchAgent ? { launchAgent: params.launchAgent } : {}),
|
||||
...(params.terminalColorQueryReplies
|
||||
? { terminalColorQueryReplies: params.terminalColorQueryReplies }
|
||||
: {}),
|
||||
title: params.title,
|
||||
focus,
|
||||
rendererBacked: params.rendererBacked === true,
|
||||
activate,
|
||||
presentation,
|
||||
tabId: params.tabId,
|
||||
leafId: params.leafId,
|
||||
...(preAllocatedHandle ? { preAllocatedHandle } : {})
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}),
|
||||
defineMethod({
|
||||
name: 'terminal.split',
|
||||
|
||||
Reference in New Issue
Block a user