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:
Brennan Benson 2026-08-04 17:50:52 -07:00 committed by GitHub
parent dc5c5a89ba
commit 39c3c58d55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 477 additions and 34 deletions

View File

@ -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[] }

View File

@ -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)}`)
}

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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)
},

View File

@ -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',

View File

@ -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 }
})
})

View File

@ -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'],

View File

@ -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, {

View File

@ -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)

View File

@ -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({

View File

@ -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

View File

@ -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>()

View File

@ -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,

View File

@ -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({

View File

@ -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)
)

View File

@ -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({

View File

@ -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,

View File

@ -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)
})
})

View File

@ -10,6 +10,7 @@ const REMOTE_BOOLEAN_FLAGS = new Set([
'help',
'inject',
'include-archived',
'include-visual-layouts',
'json',
'me',
'relations',

View File

@ -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()

View File

@ -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')

View File

@ -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(

View File

@ -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 (

View File

@ -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 }
)

View File

@ -108,7 +108,8 @@ async function waitForLegacySleepConvergence(
{
worktree: worktreeSelector,
limit: 10_000,
requireFreshPtyLiveness: true
requireFreshPtyLiveness: true,
includeVisualLayouts: false
},
{ timeoutMs: listTimeoutMs }
)

View File

@ -91,7 +91,8 @@ async function recoverTerminalOrphans(
params: {
worktree: toRuntimeWorktreeSelector(snapshot.worktree),
handles: [...candidateHandles],
requireFreshPtyLiveness: true
requireFreshPtyLiveness: true,
includeVisualLayouts: false
},
timeoutMs: 15_000
})