mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 16:02:24 +00:00
fix(omp): negotiate keyboard support for background paired launches
This commit is contained in:
@@ -2,14 +2,14 @@
|
||||
|
||||
OMP's ProcessTerminal sends `CSI ? u` and then a DA1 sentinel before selecting its keyboard encoding. A fresh desktop terminal already advertises Kitty support, but a direct New Tab launch can query before that renderer owns replies. Previously startup ingress answered only OSC color queries.
|
||||
|
||||
The renderer now supplies optional `terminalKittyKeyboardProtocol: true` from its actual xterm `vtExtensions.kittyKeyboard` setting. The existing local/SSH/paired spawn route places it in startup ingress as optional `kittyKeyboardProtocol`. Missing or false capability leaves behavior unchanged, including panes that deliberately withhold Kitty on native Windows. The ingress version and stream opcodes do not change. Terminal creation accepts additive fields, but host-authoritative `terminal.createAgentSession` and `terminal.ensureAgentSession` use strict schemas. Clients send the keyboard flag on those methods only after the host advertises `agent-session.keyboard.v1`. The negotiated payload stays fixed across launch retries; old hosts receive the original payload and retain the renderer fallback. New hosts accept older clients that omit the flag.
|
||||
The renderer now supplies optional `terminalKittyKeyboardProtocol: true` from its actual xterm `vtExtensions.kittyKeyboard` setting. The existing local/SSH/paired spawn route places it in startup ingress as optional `kittyKeyboardProtocol`. Missing or false capability leaves behavior unchanged, including panes that deliberately withhold Kitty on native Windows. The ingress version and stream opcodes do not change. Terminal creation accepts additive fields, but host-authoritative `terminal.createAgentSession` and `terminal.ensureAgentSession` use strict schemas. Clients send the keyboard flag on those methods only after the host advertises `agent-session.keyboard.v1`. The negotiated payload stays fixed across launch retries; old hosts receive the original payload and retain the renderer fallback. New hosts accept older clients that omit the flag. Paired background launches use the same negotiated support and default paired-terminal advertisement; legacy terminal creation receives the additive flag.
|
||||
|
||||
Source ingress answers only the exact first `CSI ? u` before its deadline/renderer handoff. It uses the existing mode tracker for preceding flag pushes and the existing reply-delivery echo guard. Its transformed source span consumes the query once, while the following DA1 and Kitty mode-setting bytes retain their sequence ranges and reach the renderer. Keyboard intent does not require theme colors. Color and Kitty authority end independently: answering both colors does not end Kitty handling, and ConPTY's persistent color ownership does not retain Kitty ownership after handoff.
|
||||
|
||||
Run the actual OMP protocol smoke with a read-only reference checkout:
|
||||
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 bun tests/tools/omp-startup-keyboard-smoke.mjs /path/to/oh-my-pi
|
||||
ORCA_BACKGROUND_LAUNCH=1 bun tests/tools/omp-startup-keyboard-smoke.mjs /path/to/oh-my-pi > /tmp/omp-startup-keyboard.json
|
||||
```
|
||||
|
||||
This uses OMP's real ProcessTerminal with intercepted process-local stdin/stdout, a disposable HOME, and no model call. It verifies negotiation before any renderer attaches, a single reply, preserved mode push, and contiguous raw sequence coverage. It does not constitute live Windows/SSH or rendered shortcut proof.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createCompatibleRuntimeStatusResponseIfNeeded } from '@/runtime/runtime-compatibility-test-fixture'
|
||||
import { AGENT_SESSION_KEYBOARD_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
AGENT_BACKGROUND_SESSION_UUID_RE as UUID_RE,
|
||||
@@ -384,76 +386,116 @@ describe('launchAgentBackgroundSession remote runtime and SSH startup delivery',
|
||||
}
|
||||
})
|
||||
|
||||
it('creates background sessions on the active runtime environment', async () => {
|
||||
it.each([true, false])(
|
||||
'creates background sessions with negotiated keyboard support: %s',
|
||||
async (keyboardSupported) => {
|
||||
useRemoteAgentBackgroundRuntime(state)
|
||||
mockRuntimeEnvironmentTransportCall.mockImplementation((request: { method: string }) => {
|
||||
const status = createCompatibleRuntimeStatusResponseIfNeeded(request)
|
||||
if (status?.ok) {
|
||||
return Promise.resolve({
|
||||
...status,
|
||||
result: {
|
||||
...status.result,
|
||||
capabilities: status.result.capabilities?.filter(
|
||||
(capability) =>
|
||||
keyboardSupported || capability !== AGENT_SESSION_KEYBOARD_RUNTIME_CAPABILITY
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
return mockRuntimeEnvironmentCall(request)
|
||||
})
|
||||
const { launchAgentBackgroundSession } = await import('./launch-agent-background-session')
|
||||
|
||||
const result = await launchAgentBackgroundSession({
|
||||
agent: 'claude',
|
||||
worktreeId: 'wt-1',
|
||||
prompt: 'run the automation'
|
||||
})
|
||||
|
||||
expect(mockSpawn).not.toHaveBeenCalled()
|
||||
const params = mockRuntimeEnvironmentCall.mock.calls[0]?.[0]?.params
|
||||
if (keyboardSupported) {
|
||||
expect(params).toHaveProperty('terminalKittyKeyboardProtocol', true)
|
||||
} else {
|
||||
expect(params).not.toHaveProperty('terminalKittyKeyboardProtocol')
|
||||
}
|
||||
const leafId = params?.placement?.leafId
|
||||
const tabId = params?.placement?.tabId
|
||||
expect(leafId).toMatch(UUID_RE)
|
||||
expect(tabId).toMatch(UUID_RE)
|
||||
// Why: background launches have no explicit recipe override, so remote host settings win.
|
||||
expect(params).not.toHaveProperty('agentArgs')
|
||||
expect(mockRegisterAgentLaunchConfig).toHaveBeenCalledWith(
|
||||
`${tabId}:${leafId}`,
|
||||
{
|
||||
agentCommand: "claude '--dangerously-skip-permissions'",
|
||||
agentArgs: '--dangerously-skip-permissions',
|
||||
agentEnv: {}
|
||||
},
|
||||
{
|
||||
agentType: 'claude',
|
||||
launchToken: expect.stringMatching(UUID_RE),
|
||||
tabId,
|
||||
leafId
|
||||
}
|
||||
)
|
||||
expect(mockSetTabLayout).toHaveBeenCalledWith(
|
||||
tabId,
|
||||
expect.objectContaining({
|
||||
root: { type: 'leaf', leafId },
|
||||
activeLeafId: leafId,
|
||||
ptyIdsByLeafId: { [leafId]: 'remote:env-1@@terminal-1' }
|
||||
})
|
||||
)
|
||||
expect(mockRuntimeEnvironmentCall).toHaveBeenCalledWith({
|
||||
selector: 'env-1',
|
||||
method: 'terminal.createAgentSession',
|
||||
params: expect.objectContaining({
|
||||
clientOperationId: expect.stringMatching(/^\d{13}-[0-9a-f]{32}$/),
|
||||
worktree: 'id:wt-1',
|
||||
agent: 'claude',
|
||||
prompt: 'run the automation',
|
||||
promptDelivery: 'auto-submit',
|
||||
placement: { tabId, leafId },
|
||||
presentation: 'background'
|
||||
}),
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(mockUpdateTabPtyId).toHaveBeenCalledWith(tabId, 'remote:env-1@@terminal-1')
|
||||
expect(mockRegisterEagerPtyBuffer).not.toHaveBeenCalled()
|
||||
expect(mockRuntimeEnvironmentSubscribe).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
selector: 'env-1',
|
||||
method: 'terminal.multiplex',
|
||||
params: {}
|
||||
}),
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
tabId,
|
||||
paneKey: `${tabId}:${leafId}`,
|
||||
ptyId: 'remote:env-1@@terminal-1',
|
||||
terminalOwnership: null
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('advertises keyboard support for a background OMP launch', async () => {
|
||||
useRemoteAgentBackgroundRuntime(state)
|
||||
const { launchAgentBackgroundSession } = await import('./launch-agent-background-session')
|
||||
|
||||
const result = await launchAgentBackgroundSession({
|
||||
agent: 'claude',
|
||||
worktreeId: 'wt-1',
|
||||
prompt: 'run the automation'
|
||||
})
|
||||
|
||||
expect(mockSpawn).not.toHaveBeenCalled()
|
||||
const params = mockRuntimeEnvironmentCall.mock.calls[0]?.[0]?.params
|
||||
const leafId = params?.placement?.leafId
|
||||
const tabId = params?.placement?.tabId
|
||||
expect(leafId).toMatch(UUID_RE)
|
||||
expect(tabId).toMatch(UUID_RE)
|
||||
// Why: background launches have no explicit recipe override, so remote host settings win.
|
||||
expect(params).not.toHaveProperty('agentArgs')
|
||||
expect(mockRegisterAgentLaunchConfig).toHaveBeenCalledWith(
|
||||
`${tabId}:${leafId}`,
|
||||
{
|
||||
agentCommand: "claude '--dangerously-skip-permissions'",
|
||||
agentArgs: '--dangerously-skip-permissions',
|
||||
agentEnv: {}
|
||||
},
|
||||
{
|
||||
agentType: 'claude',
|
||||
launchToken: expect.stringMatching(UUID_RE),
|
||||
tabId,
|
||||
leafId
|
||||
}
|
||||
)
|
||||
expect(mockSetTabLayout).toHaveBeenCalledWith(
|
||||
tabId,
|
||||
await launchAgentBackgroundSession({ agent: 'omp', worktreeId: 'wt-1', prompt: 'review' })
|
||||
expect(mockRuntimeEnvironmentCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
root: { type: 'leaf', leafId },
|
||||
activeLeafId: leafId,
|
||||
ptyIdsByLeafId: { [leafId]: 'remote:env-1@@terminal-1' }
|
||||
method: 'terminal.createAgentSession',
|
||||
params: expect.objectContaining({
|
||||
agent: 'omp',
|
||||
terminalKittyKeyboardProtocol: true,
|
||||
presentation: 'background'
|
||||
})
|
||||
})
|
||||
)
|
||||
expect(mockRuntimeEnvironmentCall).toHaveBeenCalledWith({
|
||||
selector: 'env-1',
|
||||
method: 'terminal.createAgentSession',
|
||||
params: expect.objectContaining({
|
||||
clientOperationId: expect.stringMatching(/^\d{13}-[0-9a-f]{32}$/),
|
||||
worktree: 'id:wt-1',
|
||||
agent: 'claude',
|
||||
prompt: 'run the automation',
|
||||
promptDelivery: 'auto-submit',
|
||||
placement: { tabId, leafId },
|
||||
presentation: 'background'
|
||||
}),
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(mockUpdateTabPtyId).toHaveBeenCalledWith(tabId, 'remote:env-1@@terminal-1')
|
||||
expect(mockRegisterEagerPtyBuffer).not.toHaveBeenCalled()
|
||||
expect(mockRuntimeEnvironmentSubscribe).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
selector: 'env-1',
|
||||
method: 'terminal.multiplex',
|
||||
params: {}
|
||||
}),
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
tabId,
|
||||
paneKey: `${tabId}:${leafId}`,
|
||||
ptyId: 'remote:env-1@@terminal-1',
|
||||
terminalOwnership: null
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves the legacy background spawn on an old remote host', async () => {
|
||||
@@ -494,6 +536,7 @@ describe('launchAgentBackgroundSession remote runtime and SSH startup delivery',
|
||||
params: expect.objectContaining({
|
||||
worktree: 'id:wt-1',
|
||||
command: "claude '--dangerously-skip-permissions' 'run remotely'",
|
||||
terminalKittyKeyboardProtocol: true,
|
||||
launchAgent: 'claude',
|
||||
presentation: 'background'
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { buildDefaultTerminalOptions } from '@/lib/pane-manager/pane-terminal-options'
|
||||
import { createAgentSessionKeyboardOptions } from '@/runtime/agent-session-keyboard-capability'
|
||||
import type { SleepingAgentLaunchConfig } from '../../../shared/agent-session-resume'
|
||||
import type { StartupCommandDelivery } from '../../../shared/codex-startup-delivery'
|
||||
import type { SessionOptionValue } from '../../../shared/native-chat-session-options'
|
||||
@@ -29,17 +31,20 @@ export async function createRuntimeAgentBackgroundTerminal(args: {
|
||||
title?: string
|
||||
}
|
||||
}): Promise<{ terminal: RuntimeTerminalCreate }> {
|
||||
const keyboardProtocol = buildDefaultTerminalOptions().vtExtensions?.kittyKeyboard
|
||||
const keyboardOptions = createAgentSessionKeyboardOptions(keyboardProtocol)
|
||||
const operation = createAgentSessionCreateOperation()
|
||||
const launchPreferences = toAgentLaunchPreferences(args.sessionOptions)
|
||||
return await runRemoteAgentSessionLaunch({
|
||||
environmentId: args.environmentId,
|
||||
hostAuthority: () =>
|
||||
operation.run((clientOperationId) =>
|
||||
operation.run(async (clientOperationId) =>
|
||||
callRuntimeRpc<{ terminal: RuntimeTerminalCreate }>(
|
||||
{ kind: 'environment', environmentId: args.environmentId },
|
||||
'terminal.createAgentSession',
|
||||
withAgentSessionCreateOperationId(
|
||||
{
|
||||
...(await keyboardOptions(args.environmentId)),
|
||||
worktree: toRuntimeWorktreeSelector(args.worktreeId),
|
||||
agent: args.agent,
|
||||
...(args.prompt
|
||||
@@ -62,6 +67,7 @@ export async function createRuntimeAgentBackgroundTerminal(args: {
|
||||
{
|
||||
worktree: toRuntimeWorktreeSelector(args.worktreeId),
|
||||
command: args.legacy.command,
|
||||
...(keyboardProtocol === true ? { terminalKittyKeyboardProtocol: true } : {}),
|
||||
...(args.legacy.startupCommandDelivery
|
||||
? { startupCommandDelivery: args.legacy.startupCommandDelivery }
|
||||
: {}),
|
||||
|
||||
Reference in New Issue
Block a user