perf(runtime): gate terminal.list visual layouts (#12450)
* perf(runtime): gate terminal.list visual layouts and stop the false writable claim visualLayouts is ~31% of a large terminal.list payload (44,208 B of 137,412 B on a live 134-terminal remote runtime) and has exactly one consumer: the human-readable CLI formatter. Gate it behind an includeVisualLayouts request param that defaults to included, so pre-flag clients are unaffected, and have every --json/internal caller opt out. Also drop the record-backed builder's writable, which was a verbatim copy of connected. terminal.show now states writability explicitly as exactly what terminal.send's PTY gate enforces. * test(runtime): type the payload-size fixture arrays for tsc * fix(runtime): preserve terminal list compatibility * test(runtime): guard terminal list optimization * fix(cli): preserve agent access to terminal layouts
This commit is contained in:
parent
dc5c5a89ba
commit
39c3c58d55
|
|
@ -1693,7 +1693,8 @@ export default function SessionScreen() {
|
|||
|
||||
try {
|
||||
const response = await client.sendRequest('terminal.list', {
|
||||
worktree: `id:${worktreeId}`
|
||||
worktree: `id:${worktreeId}`,
|
||||
includeVisualLayouts: false
|
||||
})
|
||||
if (response.ok) {
|
||||
const result = (response as RpcSuccess).result as { terminals: Terminal[] }
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ async function chooseWorktree(ws: WebSocket): Promise<string> {
|
|||
}
|
||||
|
||||
async function chooseTerminal(ws: WebSocket, worktree: string): Promise<string> {
|
||||
const list = await send(ws, 'terminal.list', { worktree })
|
||||
const list = await send(ws, 'terminal.list', { worktree, includeVisualLayouts: false })
|
||||
if (!list.ok) {
|
||||
throw new Error(`terminal.list failed: ${formatResponse(list)}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ ORCA terminal close --terminal <handle> --json
|
|||
Terminal rules:
|
||||
|
||||
- `--terminal` is optional for most commands; omitted means the active terminal in the current worktree.
|
||||
- `terminal list --json` omits `visualLayouts` to keep the common agent payload bounded. Add `--include-visual-layouts` only when tab and pane topology is required.
|
||||
- Use `terminal read` before `terminal send` unless the next input is obvious.
|
||||
- Use `terminal send` only for direct terminal input or one-off prompts where no task state, inbox, or reply tracking is needed.
|
||||
- For structured coordination, invoke the `orchestration` skill; it uses `orca orchestration ...` commands for messages, handoffs, task DAGs, dispatches, inbox/reply flows, and coordinator loops. A receiving agent can run `orca orchestration check --unread --inject` to render its unread mail in agent-readable form; this checks the caller's inbox and does not remotely deliver input to another terminal.
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ Rules:
|
|||
- Use `--peek` and `--all` only for read-only history/debugging. Type filters decide when a waiter wakes; the returned actionable Delivery is still the oldest full batch.
|
||||
- Use `dispatch:<id>` for coordinator guidance to one supervised worker. Orca routes that stable address locally or through the connected-server relay; do not substitute a remote terminal handle.
|
||||
- Terminal handles remain appropriate for low-level pre-Dispatch messaging. Prefer `agentTerminalHandle` from the create response, fall back to `startupTerminal.handle` for older runtimes, then re-resolve with `orca terminal list --worktree ... --json` if missing or stale. Continue with the replacement handle only; never dual-send to old and new handles.
|
||||
- `terminal list --json` omits `visualLayouts` because handle recovery does not need topology. Add `--include-visual-layouts` only for explicit tab and pane inspection.
|
||||
- `orca orchestration check --peek --format --json` returns locally formatted unread mail without consuming it; it never writes to terminal input or remotely wakes another terminal. Use `orchestration dispatch --inject` to deliver a tracked task, or `terminal send` when an existing agent needs a free-form prompt.
|
||||
- While supervising workers manually, use `check --wait --types worker_done,escalation,question --timeout-ms <n>` instead of sleep/poll loops. Process the whole Delivery, reply to `question` messages with `orca orchestration reply --id <msg_id> --body <answer> --json`, then acknowledge and keep waiting.
|
||||
- Treat a `check --wait` timeout or `{count:0}` as a checkpoint, not a worker failure. Long coding tasks routinely run 15-60 minutes; keep using rolling waits unless you receive `worker_done`/`escalation`, the terminal exits or disappears, or the user explicitly asks you to stop.
|
||||
|
|
@ -355,7 +356,7 @@ Sidebar lineage and orchestration lifecycle are related but not identical. A sam
|
|||
Other terminal commands coordinators often need:
|
||||
|
||||
```bash
|
||||
orca terminal list [--worktree <selector>] [--json]
|
||||
orca terminal list [--worktree <selector>] [--include-visual-layouts] [--json]
|
||||
orca terminal create [--worktree <selector>] [--title <text>] [--command <cmd>] [--json]
|
||||
orca terminal split --terminal <handle> [--direction horizontal|vertical] [--command <cmd>] [--json]
|
||||
orca terminal wait --terminal <handle> --for tui-idle --timeout-ms <n> --json
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export const BOOLEAN_FLAGS = new Set([
|
|||
'help',
|
||||
'inject',
|
||||
'include-archived',
|
||||
'include-visual-layouts',
|
||||
'interrupt',
|
||||
'json',
|
||||
'local',
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -55,7 +55,9 @@ export const TERMINAL_HANDLERS: Record<string, CommandHandler> = {
|
|||
'terminal list': async ({ flags, client, cwd, json }) => {
|
||||
const result = await client.call<RuntimeTerminalListResult>('terminal.list', {
|
||||
worktree: await getOptionalWorktreeSelector(flags, 'worktree', cwd, client),
|
||||
limit: getOptionalPositiveIntegerFlag(flags, 'limit')
|
||||
limit: getOptionalPositiveIntegerFlag(flags, 'limit'),
|
||||
// Why: agent JSON calls dominate; topology stays available through an explicit opt-in.
|
||||
includeVisualLayouts: !json || flags.has('include-visual-layouts')
|
||||
})
|
||||
printResult(result, json, formatTerminalList)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ Common Commands:
|
|||
orca file open <path> [--worktree <selector>] [--json]
|
||||
orca file diff <path> [--staged] [--worktree <selector>] [--json]
|
||||
orca file open-changed [--mode edit|diff|both] [--worktree <selector>] [--json]
|
||||
orca terminal list [--worktree <selector>] [--limit <n>] [--json]
|
||||
orca terminal list [--worktree <selector>] [--limit <n>] [--include-visual-layouts] [--json]
|
||||
orca terminal show [--terminal <handle>] [--json]
|
||||
orca terminal read [--terminal <handle>] [--cursor <n>] [--limit <n>] [--json]
|
||||
orca terminal send [--terminal <handle>] [--text <text>] [--enter] [--interrupt] [--json]
|
||||
|
|
@ -272,6 +272,9 @@ Terminal Send Options:
|
|||
--enter Append Enter after sending text
|
||||
--interrupt Send as an interrupt-style input when supported
|
||||
|
||||
Terminal List Options:
|
||||
--include-visual-layouts Include tab and pane topology in JSON output
|
||||
|
||||
Wait Options:
|
||||
--for exit Wait until the target terminal exits
|
||||
--timeout-ms <ms> Maximum wait time before timing out
|
||||
|
|
@ -535,6 +538,8 @@ export function formatFlagHelp(flag: string): string {
|
|||
'from-x': '--from-x <x> Source window-local x coordinate',
|
||||
'from-y': '--from-y <y> Source window-local y coordinate',
|
||||
help: '--help Show this help message',
|
||||
'include-visual-layouts':
|
||||
'--include-visual-layouts Include tab and pane topology in JSON output',
|
||||
interrupt: '--interrupt Send as an interrupt-style input when supported',
|
||||
id: '--id <id> Identifier for a target item or permission',
|
||||
issue: '--issue <number|null> Linked GitHub issue number',
|
||||
|
|
|
|||
|
|
@ -426,6 +426,19 @@ describe('orca root help', () => {
|
|||
expect(callMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('documents the machine-readable terminal topology opt-in', async () => {
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
logSpy.mockClear()
|
||||
|
||||
await main(['terminal', 'list', '--help'], '/tmp/repo')
|
||||
|
||||
const help = String(logSpy.mock.calls[0][0])
|
||||
expect(help).toContain('[--include-visual-layouts] [--json]')
|
||||
expect(help).toContain('--include-visual-layouts Include tab and pane topology in JSON output')
|
||||
expect(help).toContain('JSON omits visualLayouts by default')
|
||||
expect(callMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('describes worker-read cursors as opaque', async () => {
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
logSpy.mockClear()
|
||||
|
|
@ -3705,7 +3718,60 @@ describe('orca cli worktree awareness', () => {
|
|||
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'terminal.list', {
|
||||
worktree: 'id:repo::/tmp/repo/feature',
|
||||
limit: undefined
|
||||
limit: undefined,
|
||||
includeVisualLayouts: false
|
||||
})
|
||||
})
|
||||
|
||||
it('requests visual layouts only for the human-readable terminal list', async () => {
|
||||
queueFixtures(
|
||||
callMock,
|
||||
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
|
||||
okFixture('req_term', { terminals: [], totalCount: 0, truncated: false })
|
||||
)
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(['terminal', 'list', '--worktree', 'active'], '/tmp/repo/feature/src')
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'terminal.list',
|
||||
expect.objectContaining({ includeVisualLayouts: true })
|
||||
)
|
||||
})
|
||||
|
||||
it('allows agent JSON clients to request visual layouts explicitly', async () => {
|
||||
const visualLayouts = [
|
||||
{
|
||||
worktreeId: 'repo::/tmp/repo/feature',
|
||||
worktreePath: '/tmp/repo/feature',
|
||||
root: { type: 'group', groupId: null, activeTabId: null, tabs: [] }
|
||||
}
|
||||
]
|
||||
queueFixtures(
|
||||
callMock,
|
||||
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
|
||||
okFixture('req_term', {
|
||||
terminals: [],
|
||||
visualLayouts,
|
||||
totalCount: 0,
|
||||
truncated: false
|
||||
})
|
||||
)
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(
|
||||
['terminal', 'list', '--worktree', 'active', '--include-visual-layouts', '--json'],
|
||||
'/tmp/repo/feature/src'
|
||||
)
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'terminal.list',
|
||||
expect.objectContaining({ includeVisualLayouts: true })
|
||||
)
|
||||
expect(JSON.parse(String(logSpy.mock.calls[0]?.[0]))).toMatchObject({
|
||||
result: { visualLayouts }
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -180,8 +180,12 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [
|
|||
{
|
||||
path: ['terminal', 'list'],
|
||||
summary: 'List live Orca-managed terminals',
|
||||
usage: 'orca terminal list [--worktree <selector>] [--limit <n>] [--json]',
|
||||
allowedFlags: [...GLOBAL_FLAGS, 'worktree', 'limit']
|
||||
usage:
|
||||
'orca terminal list [--worktree <selector>] [--limit <n>] [--include-visual-layouts] [--json]',
|
||||
allowedFlags: [...GLOBAL_FLAGS, 'worktree', 'limit', 'include-visual-layouts'],
|
||||
notes: [
|
||||
'JSON omits visualLayouts by default; pass --include-visual-layouts when machine-readable tab and pane topology is required.'
|
||||
]
|
||||
},
|
||||
{
|
||||
path: ['terminal', 'show'],
|
||||
|
|
|
|||
|
|
@ -172,7 +172,8 @@ describe('terminal.sendText explicit worktree routing', () => {
|
|||
expect(delegate.listTerminals).toHaveBeenCalledTimes(1)
|
||||
expect(delegate.listTerminals).toHaveBeenCalledWith(
|
||||
'id:worktree-1',
|
||||
PLUGIN_WORKSPACE_TERMINAL_LIMIT
|
||||
PLUGIN_WORKSPACE_TERMINAL_LIMIT,
|
||||
{ includeVisualLayouts: false }
|
||||
)
|
||||
expect(delegate.sendTerminal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
|
@ -189,7 +190,8 @@ describe('terminal.sendText explicit worktree routing', () => {
|
|||
expect(delegate.listTerminals).toHaveBeenCalledTimes(1)
|
||||
expect(delegate.listTerminals).toHaveBeenCalledWith(
|
||||
'id:worktree-1',
|
||||
PLUGIN_WORKSPACE_TERMINAL_LIMIT
|
||||
PLUGIN_WORKSPACE_TERMINAL_LIMIT,
|
||||
{ includeVisualLayouts: false }
|
||||
)
|
||||
expect(delegate.sendTerminal).toHaveBeenCalledTimes(1)
|
||||
expect(delegate.sendTerminal).toHaveBeenCalledWith(terminalId, {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ export type PluginRuntimeDelegate = {
|
|||
} | null>
|
||||
listTerminals(
|
||||
worktreeSelector?: string,
|
||||
limit?: number
|
||||
limit?: number,
|
||||
opts?: { includeVisualLayouts?: boolean }
|
||||
): Promise<{ terminals: { handle: string; title: string | null }[] }>
|
||||
sendTerminal(
|
||||
handle: string,
|
||||
|
|
@ -50,7 +51,8 @@ export function bindPluginHostServices(input: {
|
|||
listWorktreeTerminals: async (worktreeId) => {
|
||||
const result = await delegate.listTerminals(
|
||||
`id:${worktreeId}`,
|
||||
PLUGIN_WORKSPACE_TERMINAL_LIMIT
|
||||
PLUGIN_WORKSPACE_TERMINAL_LIMIT,
|
||||
{ includeVisualLayouts: false }
|
||||
)
|
||||
return result.terminals
|
||||
.slice(0, PLUGIN_WORKSPACE_TERMINAL_LIMIT)
|
||||
|
|
|
|||
|
|
@ -20814,6 +20814,47 @@ describe('OrcaRuntimeService', () => {
|
|||
).rejects.toThrow('terminal_topology_conflict')
|
||||
})
|
||||
|
||||
it('keeps orphaned list and show writability aligned with the send gate', async () => {
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({
|
||||
...getDefaultWorkspaceSession(),
|
||||
tabsByWorktree: { [TEST_WORKTREE_ID]: [] }
|
||||
})
|
||||
const runtime = new OrcaRuntimeService({ ...runtimeStore, flushOrThrow: vi.fn() } as never)
|
||||
const writes: [string, string][] = []
|
||||
runtime.setPtyController({
|
||||
write: (ptyId: string, data: string) => {
|
||||
writes.push([ptyId, data])
|
||||
return true
|
||||
},
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: async () => [
|
||||
{
|
||||
id: 'pty-orphan',
|
||||
incarnationId: 'inc-orphan',
|
||||
terminalHandle: 'term_orphan',
|
||||
title: 'shell',
|
||||
cwd: TEST_WORKTREE_PATH,
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
wslDistro: null
|
||||
}
|
||||
]
|
||||
} as never)
|
||||
runtime.registerPty('pty-orphan', TEST_WORKTREE_ID)
|
||||
runtime.onPtySpawned('pty-orphan', 'inc-orphan', { awaitsRegistration: false })
|
||||
|
||||
const listed = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)
|
||||
const entry = listed.terminals.find((terminal) => terminal.ptyId === 'pty-orphan')
|
||||
expect(entry).toMatchObject({ orphaned: true, connected: true, writable: true })
|
||||
|
||||
const shown = await runtime.showTerminal(entry!.handle)
|
||||
expect(shown.writable).toBe(true)
|
||||
await expect(runtime.sendTerminal(entry!.handle, { text: 'hi' })).resolves.toMatchObject({
|
||||
accepted: true
|
||||
})
|
||||
expect(writes).toEqual([['pty-orphan', 'hi']])
|
||||
})
|
||||
|
||||
it('rejects connection mismatch and reused handles while allowing a WSL-owned orphan', async () => {
|
||||
const makeRuntime = (): OrcaRuntimeService => {
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({
|
||||
|
|
|
|||
|
|
@ -15040,7 +15040,11 @@ export class OrcaRuntimeService {
|
|||
async listTerminals(
|
||||
worktreeSelector?: string,
|
||||
limit = DEFAULT_TERMINAL_LIST_LIMIT,
|
||||
opts: { handles?: readonly string[]; requireFreshPtyLiveness?: boolean } = {}
|
||||
opts: {
|
||||
handles?: readonly string[]
|
||||
requireFreshPtyLiveness?: boolean
|
||||
includeVisualLayouts?: boolean
|
||||
} = {}
|
||||
): Promise<RuntimeTerminalListResult> {
|
||||
if (!Number.isInteger(limit) || limit <= 0) {
|
||||
throw new Error('invalid_limit')
|
||||
|
|
@ -15164,11 +15168,12 @@ export class OrcaRuntimeService {
|
|||
? terminals.filter((terminal) => requestedHandles.has(terminal.handle))
|
||||
: terminals
|
||||
const listedTerminals = matchingTerminals.slice(0, limit)
|
||||
const visualLayouts = this.buildTerminalVisualLayouts(
|
||||
listedTerminals,
|
||||
worktreesById,
|
||||
targetWorktreeId
|
||||
)
|
||||
// Why: undefined (pre-flag client) must still get layouts; only an explicit
|
||||
// `false` opts out.
|
||||
const visualLayouts =
|
||||
opts.includeVisualLayouts === false
|
||||
? []
|
||||
: this.buildTerminalVisualLayouts(listedTerminals, worktreesById, targetWorktreeId)
|
||||
|
||||
return {
|
||||
terminals: listedTerminals,
|
||||
|
|
@ -15912,7 +15917,9 @@ export class OrcaRuntimeService {
|
|||
return activeTerminal.terminal
|
||||
}
|
||||
}
|
||||
const listed = await this.listTerminals(worktreeSelector)
|
||||
const listed = await this.listTerminals(worktreeSelector, undefined, {
|
||||
includeVisualLayouts: false
|
||||
})
|
||||
const first = listed.terminals[0]?.handle
|
||||
if (first) {
|
||||
return first
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ export type CoordinatorRuntime = {
|
|||
sendTerminalAgentPrompt(handle: string, prompt: string): Promise<unknown>
|
||||
listTerminals(
|
||||
worktreeSelector?: string,
|
||||
limit?: number
|
||||
limit?: number,
|
||||
opts?: { includeVisualLayouts?: boolean }
|
||||
): Promise<{
|
||||
terminals: { handle: string; worktreeId: string; connected: boolean; writable: boolean }[]
|
||||
}>
|
||||
|
|
@ -483,7 +484,9 @@ export class Coordinator {
|
|||
|
||||
private async getAvailableTerminals(): Promise<string[]> {
|
||||
try {
|
||||
const result = await this.runtime.listTerminals(this.opts.worktree)
|
||||
const result = await this.runtime.listTerminals(this.opts.worktree, undefined, {
|
||||
includeVisualLayouts: false
|
||||
})
|
||||
const dispatched = this.db.listTasks({ status: 'dispatched' })
|
||||
const busyHandles = new Set<string>()
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,9 @@ export const ORCHESTRATION_FEDERATION_ATTACH_METHODS: RpcMethod[] = [
|
|||
created.warning ?? 'Agent-first worktree creation returned no terminal.'
|
||||
)
|
||||
}
|
||||
const listed = await runtime.listTerminals(`id:${created.worktree.id}`)
|
||||
const listed = await runtime.listTerminals(`id:${created.worktree.id}`, undefined, {
|
||||
includeVisualLayouts: false
|
||||
})
|
||||
appendFederationTerminalEffects(
|
||||
effects,
|
||||
listed.terminals,
|
||||
|
|
|
|||
|
|
@ -170,7 +170,9 @@ export async function createWorkerWorktree(args: {
|
|||
if (!terminalHandle) {
|
||||
throw new Error(created.warning ?? 'Agent-first worktree creation returned no terminal.')
|
||||
}
|
||||
const listed = await runtime.listTerminals(`id:${created.worktree.id}`)
|
||||
const listed = await runtime.listTerminals(`id:${created.worktree.id}`, undefined, {
|
||||
includeVisualLayouts: false
|
||||
})
|
||||
const setupTerminalHandle = created.setupReceipt?.terminalHandle
|
||||
for (const terminal of listed.terminals) {
|
||||
effects.push({
|
||||
|
|
|
|||
|
|
@ -634,7 +634,9 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
|
|||
}
|
||||
|
||||
// Why: fan out one message per recipient (independent read-tracking) but share a thread_id for correlation (Section 4.5).
|
||||
const { terminals } = await runtime.listTerminals()
|
||||
const { terminals } = await runtime.listTerminals(undefined, undefined, {
|
||||
includeVisualLayouts: false
|
||||
})
|
||||
const handles = resolveGroupAddress(to, from, terminals, (handle: string) =>
|
||||
runtime.getAgentStatusForHandle(handle)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -813,7 +813,10 @@ const TerminalListParams = z.object({
|
|||
.array(requiredString('Missing terminal handle').pipe(z.string().max(256)))
|
||||
.max(64)
|
||||
.optional(),
|
||||
requireFreshPtyLiveness: z.boolean().optional()
|
||||
requireFreshPtyLiveness: z.boolean().optional(),
|
||||
// Why: layouts are ~31% of a large listing and only the human CLI formatter
|
||||
// reads them. Absent means "include" so pre-flag clients keep rendering them.
|
||||
includeVisualLayouts: z.boolean().optional()
|
||||
})
|
||||
|
||||
const TerminalResolveActive = z.object({
|
||||
|
|
@ -1108,7 +1111,8 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [
|
|||
handler: async (params, { runtime }) =>
|
||||
runtime.listTerminals(params.worktree, params.limit, {
|
||||
handles: params.handles,
|
||||
requireFreshPtyLiveness: params.requireFreshPtyLiveness
|
||||
requireFreshPtyLiveness: params.requireFreshPtyLiveness,
|
||||
includeVisualLayouts: params.includeVisualLayouts
|
||||
})
|
||||
}),
|
||||
defineMethod({
|
||||
|
|
|
|||
|
|
@ -4051,6 +4051,32 @@ describe('OrcaRuntimeRpcServer', () => {
|
|||
}
|
||||
])
|
||||
|
||||
// Pins the opt-out half of the compat contract: the request above omits
|
||||
// the flag and still gets layouts; only an explicit `false` drops them.
|
||||
const optedOutResponse = await sendRequest(metadata!.transports[0]!.endpoint, {
|
||||
id: 'req_list_layout_opt_out',
|
||||
authToken: metadata!.authToken,
|
||||
method: 'terminal.list',
|
||||
params: { worktree: `id:${worktreeId}`, includeVisualLayouts: false }
|
||||
})
|
||||
const optedOut = optedOutResponse.result as {
|
||||
visualLayouts?: unknown[]
|
||||
terminals: unknown[]
|
||||
}
|
||||
expect(optedOutResponse).toMatchObject({ id: 'req_list_layout_opt_out', ok: true })
|
||||
expect(optedOut.visualLayouts).toBeUndefined()
|
||||
expect(optedOut.terminals).toHaveLength(result.terminals.length)
|
||||
|
||||
const explicitIncludeResponse = await sendRequest(metadata!.transports[0]!.endpoint, {
|
||||
id: 'req_list_layout_opt_in',
|
||||
authToken: metadata!.authToken,
|
||||
method: 'terminal.list',
|
||||
params: { worktree: `id:${worktreeId}`, includeVisualLayouts: true }
|
||||
})
|
||||
expect(
|
||||
(explicitIncludeResponse.result as { visualLayouts?: unknown[] }).visualLayouts
|
||||
).toHaveLength(1)
|
||||
|
||||
const resolvePaneResponse = await sendRequest(metadata!.transports[0]!.endpoint, {
|
||||
id: 'req_resolve_pane',
|
||||
authToken: metadata!.authToken,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,224 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
|
||||
// Measures the real `terminal.list` wire payload for a large listing, so the
|
||||
// visualLayouts opt-out keeps paying for itself. Sized against a live
|
||||
// 134-terminal remote runtime (137,412 B on the wire, visualLayouts 44,208 B).
|
||||
const TERMINAL_COUNT = 134
|
||||
const PANES_PER_TAB = 2
|
||||
const TAB_COUNT = TERMINAL_COUNT / PANES_PER_TAB
|
||||
// Leaves ~19% headroom over the measured 75,467 B while catching material bloat.
|
||||
const MAX_OPTED_OUT_PAYLOAD_BYTES = 90_000
|
||||
const REPO_ID = 'repo-7f3a91c2e4b85d60'
|
||||
const WORKTREE_PATH = '/Users/dev/orca/workspaces/orca/perf-terminal-list-diet'
|
||||
const WORKTREE_ID = `${REPO_ID}::${WORKTREE_PATH}`
|
||||
|
||||
const uuid = (n: number): string => {
|
||||
const hex = n.toString(16).padStart(12, '0')
|
||||
return `11111111-1111-4111-8111-${hex}`
|
||||
}
|
||||
|
||||
const leafIdFor = (index: number): string => uuid(index)
|
||||
const tabIdFor = (tab: number): string => `tab-${uuid(1_000 + tab)}`
|
||||
const ptyIdFor = (index: number): string => `pty-${uuid(2_000 + index)}`
|
||||
const groupIdFor = (tab: number): string => `group-${uuid(3_000 + (tab % 2))}`
|
||||
const titleFor = (index: number): string => `claude — orca/perf-terminal-list-diet #${index}`
|
||||
|
||||
const makeStore = () => ({
|
||||
getRepo: (id: string) => makeStore().getRepos()[0] ?? (id as never),
|
||||
getRepos: () => [
|
||||
{ id: REPO_ID, path: '/tmp/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1 }
|
||||
],
|
||||
addRepo: () => {},
|
||||
updateRepo: (id: string) => makeStore().getRepo(id) as never,
|
||||
getAllWorktreeMeta: () => ({
|
||||
[WORKTREE_ID]: {
|
||||
displayName: 'foo',
|
||||
comment: '',
|
||||
linkedIssue: 123,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0
|
||||
}
|
||||
}),
|
||||
getWorktreeMeta: (worktreeId: string) =>
|
||||
(makeStore().getAllWorktreeMeta() as Record<string, unknown>)[worktreeId] as never,
|
||||
setWorktreeMeta: () => ({}) as never,
|
||||
removeWorktreeMeta: () => {},
|
||||
getSettings: () => ({
|
||||
workspaceDir: '/tmp/workspaces',
|
||||
nestWorkspaces: false,
|
||||
branchPrefix: 'none',
|
||||
branchPrefixCustom: ''
|
||||
})
|
||||
})
|
||||
|
||||
type PaneLayout =
|
||||
| { type: 'leaf'; leafId: string }
|
||||
| { type: 'split'; direction: 'vertical'; first: PaneLayout; second: PaneLayout }
|
||||
|
||||
type GraphTab = {
|
||||
tabId: string
|
||||
worktreeId: string
|
||||
title: string
|
||||
activeLeafId: string
|
||||
layout: PaneLayout
|
||||
}
|
||||
|
||||
type GraphLeaf = {
|
||||
tabId: string
|
||||
worktreeId: string
|
||||
leafId: string
|
||||
paneRuntimeId: number
|
||||
ptyId: string
|
||||
title: string
|
||||
}
|
||||
|
||||
type MobileTab = {
|
||||
type: 'terminal'
|
||||
id: string
|
||||
title: string
|
||||
parentTabId: string
|
||||
leafId: string
|
||||
ptyId: string
|
||||
parentLayout: {
|
||||
root: PaneLayout
|
||||
activeLeafId: string
|
||||
expandedLeafId: string | null
|
||||
ptyIdsByLeafId: Record<string, string>
|
||||
}
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
function buildLoadedRuntime(): OrcaRuntimeService {
|
||||
const runtime = new OrcaRuntimeService(makeStore() as never)
|
||||
const tabs: GraphTab[] = []
|
||||
const leaves: GraphLeaf[] = []
|
||||
const mobileTabs: MobileTab[] = []
|
||||
|
||||
for (let tab = 0; tab < TAB_COUNT; tab += 1) {
|
||||
const tabId = tabIdFor(tab)
|
||||
const first = leafIdFor(tab * PANES_PER_TAB)
|
||||
const second = leafIdFor(tab * PANES_PER_TAB + 1)
|
||||
const root: PaneLayout = {
|
||||
type: 'split',
|
||||
direction: 'vertical',
|
||||
first: { type: 'leaf', leafId: first },
|
||||
second: { type: 'leaf', leafId: second }
|
||||
}
|
||||
tabs.push({
|
||||
tabId,
|
||||
worktreeId: WORKTREE_ID,
|
||||
title: `Tab ${tab}`,
|
||||
activeLeafId: second,
|
||||
layout: root
|
||||
})
|
||||
for (let pane = 0; pane < PANES_PER_TAB; pane += 1) {
|
||||
const index = tab * PANES_PER_TAB + pane
|
||||
const leafId = leafIdFor(index)
|
||||
leaves.push({
|
||||
tabId,
|
||||
worktreeId: WORKTREE_ID,
|
||||
leafId,
|
||||
paneRuntimeId: pane + 1,
|
||||
ptyId: ptyIdFor(index),
|
||||
title: titleFor(index)
|
||||
})
|
||||
mobileTabs.push({
|
||||
type: 'terminal',
|
||||
id: `${tabId}::${leafId}`,
|
||||
title: titleFor(index),
|
||||
parentTabId: tabId,
|
||||
leafId,
|
||||
ptyId: ptyIdFor(index),
|
||||
parentLayout: {
|
||||
root,
|
||||
activeLeafId: second,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: {
|
||||
[first]: ptyIdFor(tab * PANES_PER_TAB),
|
||||
[second]: ptyIdFor(tab * PANES_PER_TAB + 1)
|
||||
}
|
||||
},
|
||||
isActive: index === 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const groupIds = [groupIdFor(0), groupIdFor(1)]
|
||||
runtime.attachWindow(1)
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs,
|
||||
leaves,
|
||||
mobileSessionTabs: [
|
||||
{
|
||||
worktree: WORKTREE_ID,
|
||||
publicationEpoch: 'test',
|
||||
snapshotVersion: 1,
|
||||
activeGroupId: groupIds[0],
|
||||
activeTabId: mobileTabs[0]!.id,
|
||||
activeTabType: 'terminal',
|
||||
tabGroups: groupIds.map((id, groupIndex) => ({
|
||||
id,
|
||||
activeTabId: tabIdFor(groupIndex),
|
||||
tabOrder: tabs
|
||||
.filter((_tab, tabIndex) => tabIndex % 2 === groupIndex)
|
||||
.map((tab) => tab.tabId)
|
||||
})),
|
||||
tabGroupLayout: {
|
||||
type: 'split',
|
||||
direction: 'horizontal',
|
||||
first: { type: 'leaf', groupId: groupIds[0] },
|
||||
second: { type: 'leaf', groupId: groupIds[1] }
|
||||
},
|
||||
tabs: mobileTabs
|
||||
}
|
||||
]
|
||||
} as never)
|
||||
|
||||
// Realistic previews: the live sample carried ~27 B of tail text per terminal.
|
||||
for (let index = 0; index < TERMINAL_COUNT; index += 1) {
|
||||
const ptyId = ptyIdFor(index)
|
||||
runtime.registerPty(ptyId, WORKTREE_ID)
|
||||
runtime.onPtySpawned(ptyId, `inc-${uuid(4_000 + index)}`, { awaitsRegistration: false })
|
||||
runtime.onPtyData(ptyId, `esc to interrupt · ${index}\n`, 1)
|
||||
}
|
||||
return runtime
|
||||
}
|
||||
|
||||
describe('terminal.list payload size', () => {
|
||||
it('drops ~30% of the wire payload when a caller opts out of visualLayouts', async () => {
|
||||
const runtime = buildLoadedRuntime()
|
||||
|
||||
const withLayouts = await runtime.listTerminals(`id:${WORKTREE_ID}`, 10_000)
|
||||
const layoutBuilder = vi.fn(() => [])
|
||||
Object.defineProperty(runtime, 'buildTerminalVisualLayouts', { value: layoutBuilder })
|
||||
const withoutLayouts = await runtime.listTerminals(`id:${WORKTREE_ID}`, 10_000, {
|
||||
includeVisualLayouts: false
|
||||
})
|
||||
const { visualLayouts, ...expectedWithoutLayouts } = withLayouts
|
||||
|
||||
expect(withLayouts.terminals).toHaveLength(TERMINAL_COUNT)
|
||||
expect(visualLayouts).toHaveLength(1)
|
||||
expect(withoutLayouts).toEqual(expectedWithoutLayouts)
|
||||
expect(layoutBuilder).not.toHaveBeenCalled()
|
||||
|
||||
const beforeBytes = Buffer.byteLength(JSON.stringify(withLayouts), 'utf8')
|
||||
const afterBytes = Buffer.byteLength(JSON.stringify(withoutLayouts), 'utf8')
|
||||
const layoutBytes = Buffer.byteLength(JSON.stringify(withLayouts.visualLayouts), 'utf8')
|
||||
const reduction = (beforeBytes - afterBytes) / beforeBytes
|
||||
|
||||
console.log(
|
||||
`terminal.list ${TERMINAL_COUNT} terminals: ${beforeBytes} B -> ${afterBytes} B ` +
|
||||
`(-${(reduction * 100).toFixed(1)}%); visualLayouts alone ${layoutBytes} B`
|
||||
)
|
||||
|
||||
expect(beforeBytes).toBeGreaterThan(100_000)
|
||||
expect(afterBytes).toBeLessThanOrEqual(MAX_OPTED_OUT_PAYLOAD_BYTES)
|
||||
expect(reduction).toBeGreaterThan(0.25)
|
||||
})
|
||||
})
|
||||
|
|
@ -10,6 +10,7 @@ const REMOTE_BOOLEAN_FLAGS = new Set([
|
|||
'help',
|
||||
'inject',
|
||||
'include-archived',
|
||||
'include-visual-layouts',
|
||||
'json',
|
||||
'me',
|
||||
'relations',
|
||||
|
|
|
|||
|
|
@ -139,6 +139,42 @@ describe('runRemoteOrcaCli', () => {
|
|||
return { runtime, db }
|
||||
}
|
||||
|
||||
it.each([
|
||||
{ argv: ['terminal', 'list'], includeVisualLayouts: true },
|
||||
{ argv: ['terminal', 'list', '--json'], includeVisualLayouts: false },
|
||||
{
|
||||
argv: ['terminal', 'list', '--json', '--include-visual-layouts'],
|
||||
includeVisualLayouts: true
|
||||
},
|
||||
{
|
||||
argv: ['--include-visual-layouts', 'terminal', 'list', '--json'],
|
||||
includeVisualLayouts: true
|
||||
}
|
||||
])(
|
||||
'requests terminal layouts according to the legacy SSH output mode',
|
||||
async ({ argv, includeVisualLayouts }) => {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const listTerminals = vi.spyOn(runtime, 'listTerminals').mockResolvedValue({
|
||||
terminals: [],
|
||||
totalCount: 0,
|
||||
truncated: false
|
||||
})
|
||||
|
||||
const result = await runRemoteOrcaCli(
|
||||
runtime,
|
||||
{ argv, cwd: '/home/alice/repo', env: {} },
|
||||
LEGACY_FALLBACK_OPTIONS
|
||||
)
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(listTerminals).toHaveBeenCalledWith(undefined, undefined, {
|
||||
handles: undefined,
|
||||
requireFreshPtyLiveness: undefined,
|
||||
includeVisualLayouts
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('uses the remote ORCA_TERMINAL_HANDLE as orchestration sender identity', async () => {
|
||||
const { runtime, db } = createRuntime()
|
||||
|
||||
|
|
|
|||
|
|
@ -188,7 +188,10 @@ async function dispatchRemoteCli(
|
|||
case 'terminal list':
|
||||
return await call(dispatcher, 'terminal.list', {
|
||||
worktree: optionalRemoteCliString(parsed.flags, 'worktree'),
|
||||
limit: optionalRemoteCliNumber(parsed.flags, 'limit')
|
||||
limit: optionalRemoteCliNumber(parsed.flags, 'limit'),
|
||||
// Why: agent JSON calls dominate; topology stays available through an explicit opt-in.
|
||||
includeVisualLayouts:
|
||||
!parsed.flags.has('json') || parsed.flags.has('include-visual-layouts')
|
||||
})
|
||||
case 'orchestration send': {
|
||||
const type = optionalRemoteCliString(parsed.flags, 'type')
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ describe('active agent note send', () => {
|
|||
expect(testState.callRuntimeRpc).toHaveBeenCalledWith(
|
||||
{ kind: 'local' },
|
||||
'terminal.list',
|
||||
{ worktree: 'id:wt-1', limit: 200 },
|
||||
{ worktree: 'id:wt-1', limit: 200, includeVisualLayouts: false },
|
||||
{ timeoutMs: 15000 }
|
||||
)
|
||||
expect(testState.callRuntimeRpc).toHaveBeenCalledWith(
|
||||
|
|
|
|||
|
|
@ -167,7 +167,11 @@ export async function findActiveRuntimeTerminal(
|
|||
runtimeTarget,
|
||||
'terminal.list',
|
||||
// Why: worktree ids can look like branch names or paths; keep the lookup unambiguous.
|
||||
{ worktree: toRuntimeWorktreeSelector(worktreeId), limit: ACTIVE_AGENT_TERMINAL_LIST_LIMIT },
|
||||
{
|
||||
worktree: toRuntimeWorktreeSelector(worktreeId),
|
||||
limit: ACTIVE_AGENT_TERMINAL_LIST_LIMIT,
|
||||
includeVisualLayouts: false
|
||||
},
|
||||
{ timeoutMs }
|
||||
)
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -116,7 +116,8 @@ async function collectRuntimePtyLiveness(state: AppState): Promise<RuntimePtyLiv
|
|||
{
|
||||
worktree: toRuntimeWorktreeSelector(worktreeId),
|
||||
limit: 10_000,
|
||||
requireFreshPtyLiveness: true
|
||||
requireFreshPtyLiveness: true,
|
||||
includeVisualLayouts: false
|
||||
},
|
||||
{ timeoutMs: 10_000 }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@ async function waitForLegacySleepConvergence(
|
|||
{
|
||||
worktree: worktreeSelector,
|
||||
limit: 10_000,
|
||||
requireFreshPtyLiveness: true
|
||||
requireFreshPtyLiveness: true,
|
||||
includeVisualLayouts: false
|
||||
},
|
||||
{ timeoutMs: listTimeoutMs }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ async function recoverTerminalOrphans(
|
|||
params: {
|
||||
worktree: toRuntimeWorktreeSelector(snapshot.worktree),
|
||||
handles: [...candidateHandles],
|
||||
requireFreshPtyLiveness: true
|
||||
requireFreshPtyLiveness: true,
|
||||
includeVisualLayouts: false
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue