diff --git a/mobile/scripts/test-subscribe.ts b/mobile/scripts/test-subscribe.ts index 56b90a48c52..9e97408dee6 100644 --- a/mobile/scripts/test-subscribe.ts +++ b/mobile/scripts/test-subscribe.ts @@ -139,7 +139,7 @@ async function chooseWorktree(ws: WebSocket): Promise { } async function chooseTerminal(ws: WebSocket, worktree: string): Promise { - 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)}`) } diff --git a/src/main/plugins/plugin-host-methods.test.ts b/src/main/plugins/plugin-host-methods.test.ts index 19b12f43dd9..c6a5ff3adda 100644 --- a/src/main/plugins/plugin-host-methods.test.ts +++ b/src/main/plugins/plugin-host-methods.test.ts @@ -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, { diff --git a/src/main/plugins/plugin-host-service-bindings.ts b/src/main/plugins/plugin-host-service-bindings.ts index a9aad46d387..266b819dbae 100644 --- a/src/main/plugins/plugin-host-service-bindings.ts +++ b/src/main/plugins/plugin-host-service-bindings.ts @@ -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) diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 357d6cacf31..9965b9e8f5f 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -20759,7 +20759,7 @@ describe('OrcaRuntimeService', () => { ).rejects.toThrow('terminal_topology_conflict') }) - it('never claims writable for an orphaned listing entry, and keeps show aligned with the send gate', async () => { + it('keeps orphaned list and show writability aligned with the send gate', async () => { const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({ ...getDefaultWorkspaceSession(), tabsByWorktree: { [TEST_WORKTREE_ID]: [] } @@ -20790,12 +20790,8 @@ describe('OrcaRuntimeService', () => { 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 }) - // The listing makes no writability claim it cannot back: `writable` would - // only restate `connected` here. - expect(entry?.writable).toBeUndefined() + expect(entry).toMatchObject({ orphaned: true, connected: true, writable: true }) - // ...but the orphan is genuinely writable, so show and send must agree. const shown = await runtime.showTerminal(entry!.handle) expect(shown.writable).toBe(true) await expect(runtime.sendTerminal(entry!.handle, { text: 'hi' })).resolves.toMatchObject({ diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 89b5fb76837..c139e9c977b 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -15721,7 +15721,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 @@ -15968,9 +15970,6 @@ export class OrcaRuntimeService { return { ...summary, preview, - // Why: mirrors terminal.send's PTY gate exactly (connected only — - // orphaned handles still accept writes and adoption re-attaches them). - writable: pty.pty.connected, tabId: pty.pty.tabId ?? pty.record.tabId, leafId: parsePaneKey(pty.pty.paneKey ?? '')?.leafId ?? pty.record.leafId, paneRuntimeId: -1, @@ -15993,7 +15992,6 @@ export class OrcaRuntimeService { return { ...summary, preview, - writable: leaf.writable, paneRuntimeId: leaf.paneRuntimeId, ptyId: leaf.ptyId, rendererGraphEpoch: this.rendererGraphEpoch @@ -30591,9 +30589,7 @@ export class OrcaRuntimeService { leafId: orphaned ? `pty:${pty.ptyId}` : pane.leafId, title: getLatestPtyTitle(pty), connected: pty.connected, - // Why: no `writable` — for a PTY record it would only restate `connected`, - // which is exactly what terminal.send gates on. Callers needing the claim - // use terminal.show. + writable: pty.connected, lastOutputAt: pty.lastOutputAt, preview: pty.preview } diff --git a/src/main/runtime/orchestration/coordinator.ts b/src/main/runtime/orchestration/coordinator.ts index 4175647d02e..cd4fbd29f43 100644 --- a/src/main/runtime/orchestration/coordinator.ts +++ b/src/main/runtime/orchestration/coordinator.ts @@ -8,9 +8,10 @@ export type CoordinatorRuntime = { sendTerminalAgentPrompt(handle: string, prompt: string): Promise listTerminals( worktreeSelector?: string, - limit?: number + limit?: number, + opts?: { includeVisualLayouts?: boolean } ): Promise<{ - terminals: { handle: string; worktreeId: string; connected: boolean; writable?: boolean }[] + terminals: { handle: string; worktreeId: string; connected: boolean; writable: boolean }[] }> createTerminal( worktreeSelector?: string, @@ -483,7 +484,9 @@ export class Coordinator { private async getAvailableTerminals(): Promise { 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() @@ -501,9 +504,7 @@ export class Coordinator { t.handle !== this.opts.coordinatorHandle && !busyHandles.has(t.handle) && t.connected && - // Why: absent means the runtime makes no claim beyond `connected` - // (record-backed entry); only an explicit `false` disqualifies. - t.writable !== false + t.writable ) .map((t) => t.handle) } catch { diff --git a/src/main/runtime/rpc/methods/orchestration-federation.ts b/src/main/runtime/rpc/methods/orchestration-federation.ts index 7ee1022b5a7..ce173290848 100644 --- a/src/main/runtime/rpc/methods/orchestration-federation.ts +++ b/src/main/runtime/rpc/methods/orchestration-federation.ts @@ -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, diff --git a/src/main/runtime/rpc/methods/orchestration-worker-topology.ts b/src/main/runtime/rpc/methods/orchestration-worker-topology.ts index 6ecf8613a9b..122a5b869ba 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-topology.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-topology.ts @@ -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({ diff --git a/src/main/runtime/rpc/methods/orchestration.ts b/src/main/runtime/rpc/methods/orchestration.ts index 0bdb24543a7..22518b3717b 100644 --- a/src/main/runtime/rpc/methods/orchestration.ts +++ b/src/main/runtime/rpc/methods/orchestration.ts @@ -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) ) diff --git a/src/main/ssh/ssh-remote-orca-cli.test.ts b/src/main/ssh/ssh-remote-orca-cli.test.ts index f9bd781bd0a..654f69a367b 100644 --- a/src/main/ssh/ssh-remote-orca-cli.test.ts +++ b/src/main/ssh/ssh-remote-orca-cli.test.ts @@ -139,6 +139,34 @@ describe('runRemoteOrcaCli', () => { return { runtime, db } } + it.each([ + { argv: ['terminal', 'list'], includeVisualLayouts: true }, + { argv: ['terminal', 'list', '--json'], includeVisualLayouts: false } + ])( + '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() diff --git a/src/shared/runtime-types.ts b/src/shared/runtime-types.ts index be68e565454..6e012e60b42 100644 --- a/src/shared/runtime-types.ts +++ b/src/shared/runtime-types.ts @@ -448,9 +448,7 @@ export type RuntimeTerminalSummary = { leafId: string title: string | null connected: boolean - // Why: only graph-backed entries can claim writability independently of - // `connected`; record-backed (PTY) entries omit it rather than restate it. - writable?: boolean + writable: boolean lastOutputAt: number | null preview: string } @@ -577,9 +575,6 @@ export type RuntimeWorktreeTerminalSleepResult = { ) export type RuntimeTerminalShow = RuntimeTerminalSummary & { - // Why: `terminal show` always states writability — exactly what terminal.send's - // gate enforces, so the two can never disagree. - writable: boolean paneRuntimeId: number ptyId: string | null rendererGraphEpoch: number