From 634ca8fc49ab23a4e8ccbb6a67822e1ff79f0fbf Mon Sep 17 00:00:00 2001 From: Neil Date: Sun, 20 Sep 2026 02:36:04 -0700 Subject: [PATCH] fix(antigravity): publish captured command approvals to Chat --- .../antigravity-readiness-evidence.md | 28 +++ .../server-ingest-antigravity-screen.test.ts | 126 +++++++++++ .../server-ingest-antigravity-screen.ts | 78 +++++++ .../server/server-ingest-terminal.ts | 4 +- src/main/orcad/orcad-entry.ts | 1 + ...ravity-windows-command-allow-key.meta.json | 9 + .../antigravity-windows-command-allow-key.txt | 209 ++++++++++++++++++ ...gravity-windows-command-approval.meta.json | 9 + .../antigravity-windows-command-approval.txt | 148 +++++++++++++ ...ravity-windows-command-cancelled.meta.json | 9 + .../antigravity-windows-command-cancelled.txt | 173 +++++++++++++++ .../agent-status-store-wiring.test-fixture.ts | 4 + .../antigravity-command-approval-screen.ts | 23 ++ ...ravity-command-approval-transcript.test.ts | 94 ++++++++ ...ravity-screen-permission-publisher.test.ts | 110 +++++++++ ...antigravity-screen-permission-publisher.ts | 68 ++++++ src/main/runtime/orca-runtime-on-pty-data.ts | 3 + .../orca-runtime-preserved-branch-cleanup.ts | 5 + src/main/runtime/orca-runtime-state-fields.ts | 3 + .../orca-runtime-visible-snapshot-preview.ts | 24 ++ src/main/runtime/terminal-screen-readiness.ts | 7 +- .../startup/main-process-runtime-service.ts | 1 + 22 files changed, 1133 insertions(+), 3 deletions(-) create mode 100644 src/main/agent-hooks/server-ingest-antigravity-screen.test.ts create mode 100644 src/main/agent-hooks/server/server-ingest-antigravity-screen.ts create mode 100644 src/main/runtime/__fixtures__/antigravity-windows-command-allow-key.meta.json create mode 100644 src/main/runtime/__fixtures__/antigravity-windows-command-allow-key.txt create mode 100644 src/main/runtime/__fixtures__/antigravity-windows-command-approval.meta.json create mode 100644 src/main/runtime/__fixtures__/antigravity-windows-command-approval.txt create mode 100644 src/main/runtime/__fixtures__/antigravity-windows-command-cancelled.meta.json create mode 100644 src/main/runtime/__fixtures__/antigravity-windows-command-cancelled.txt create mode 100644 src/main/runtime/antigravity-command-approval-screen.ts create mode 100644 src/main/runtime/antigravity-command-approval-transcript.test.ts create mode 100644 src/main/runtime/antigravity-screen-permission-publisher.test.ts create mode 100644 src/main/runtime/antigravity-screen-permission-publisher.ts diff --git a/docs/reference/antigravity-readiness-evidence.md b/docs/reference/antigravity-readiness-evidence.md index 89eddc35448..65d5b5948ad 100644 --- a/docs/reference/antigravity-readiness-evidence.md +++ b/docs/reference/antigravity-readiness-evidence.md @@ -371,3 +371,31 @@ without transcript truncation. The final identity-checked implementation passed another delayed launch with an exact 920-character first-turn match. Generation still failed with the existing 401 authentication error; this verifies delivery, not a successful continuation task. Real Windows/WSL/SSH runs remain unverified. + +## Native Windows command approval — 1.2.7 (2026-09-20) + +`antigravity-windows-command-approval.txt` records an actual authenticated tool +turn, stopped while the four-choice “Run this command?” dialog owns the screen. +`antigravity-windows-command-cancelled.txt` records another harmless command turn +and Escape dismissing that dialog back to the empty composer. Both used the +repository PTY recorder on native Windows at 120×40, with account information +hidden. The command executable's OS username is replaced with a same-length +placeholder in the transcript and sidecar. Both pass the transcript secret scan. + +The approval screen ends with the navigation hint and `esc to cancel`; its four +choices distinguish it from ordinary working output. The runtime regression +previously timed out without a blocked reason. The current screen classifier +reports `agent-approval-prompt`; the cancelled capture must become ready even +though its retained output contains the earlier menu. + +This establishes readiness detection only. Hook-owned permission publication, +Chat approval controls, selection changes, narrower widths and other platforms +still need verification. Do not infer permission from `PreToolUse`: agy can +execute an already-allowed tool without waiting for a user decision. + +`antigravity-windows-command-allow-key.txt` additionally records the pending menu, +sending the single key `1` (no Enter), successful harmless command execution and +return to the empty composer. This verifies the existing Chat approval card's +one-time acceptance key on native Windows 1.2.7. The recorder metadata confirms +completion; the capture was transferred as raw UTF-8 bytes without newline +normalization and its username was replaced with a same-length placeholder. diff --git a/src/main/agent-hooks/server-ingest-antigravity-screen.test.ts b/src/main/agent-hooks/server-ingest-antigravity-screen.test.ts new file mode 100644 index 00000000000..5df0be3091f --- /dev/null +++ b/src/main/agent-hooks/server-ingest-antigravity-screen.test.ts @@ -0,0 +1,126 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { AgentHookServer, _internals } from './server' +import { PANE } from './server.test-fixtures' + +vi.mock('../telemetry/client', () => ({ track: vi.fn() })) +vi.mock('../telemetry/cohort-classifier', () => ({ getCohortAtEmit: vi.fn(() => ({})) })) + +beforeEach(() => _internals.resetCachesForTests()) + +function workingPane( + agentType = 'antigravity', + connectionId: string | null = null +): AgentHookServer { + const server = new AgentHookServer() + server.ingestTerminalStatus({ + paneKey: PANE, + terminalHandle: 'term-screen', + connectionId, + payload: { state: 'working', agentType, prompt: 'print marker', toolName: 'run_command' } + }) + return server +} + +function baseline(server: AgentHookServer) { + const row = server.getStatusSnapshot()[0] + if (!row) { + throw new Error('Missing test status row') + } + return row +} + +describe('host-owned Antigravity screen permission', () => { + it('publishes permission through the canonical store and clears only its own prompt', () => { + const server = workingPane() + const listener = vi.fn() + server.setListener(listener) + listener.mockClear() + expect( + server.ingestAntigravityScreenPermission({ baseline: baseline(server), command: 'echo OK' }) + ).toBe(true) + expect(baseline(server)).toMatchObject({ + state: 'waiting', + toolName: 'run_command', + observation: { origin: 'process' } + }) + expect(JSON.parse(baseline(server).interactivePrompt ?? '')).toEqual({ + approval: { source: 'antigravity-screen', tool: 'run_command', summary: 'echo OK' } + }) + expect(listener).toHaveBeenCalledTimes(1) + expect( + server.ingestAntigravityScreenPermission({ baseline: baseline(server), command: null }) + ).toBe(true) + expect(baseline(server)).toMatchObject({ state: 'working', prompt: 'print marker' }) + expect(baseline(server).interactivePrompt).toBeUndefined() + expect(listener).toHaveBeenCalledTimes(2) + }) + + it('does not overwrite a newer provider state with a late snapshot', () => { + const server = workingPane() + const old = baseline(server) + server.ingestTerminalStatus({ + paneKey: PANE, + terminalHandle: 'term-screen', + payload: { state: 'done', agentType: 'antigravity', prompt: 'print marker' } + }) + expect(server.ingestAntigravityScreenPermission({ baseline: old, command: 'echo OK' })).toBe( + false + ) + expect(baseline(server).state).toBe('done') + }) + + it('rejects another terminal incarnation and missing observation proof', () => { + const server = workingPane() + const row = baseline(server) + expect( + server.ingestAntigravityScreenPermission({ + baseline: { ...row, terminalHandle: 'replaced' }, + command: 'echo OK' + }) + ).toBe(false) + expect( + server.ingestAntigravityScreenPermission({ + baseline: { ...row, observation: undefined }, + command: 'echo OK' + }) + ).toBe(false) + }) + + it.each([ + ['claude', null], + ['antigravity', 'ssh-connection'] + ] as const)('does not infer permission for %s on %s', (agent, connection) => { + const server = workingPane(agent, connection) + expect( + server.ingestAntigravityScreenPermission({ baseline: baseline(server), command: 'echo OK' }) + ).toBe(false) + }) + + it('does not clear a permission prompt from another producer', () => { + const server = workingPane() + server.ingestTerminalStatus({ + paneKey: PANE, + terminalHandle: 'term-screen', + payload: { + state: 'waiting', + agentType: 'antigravity', + prompt: 'print marker', + interactivePrompt: '{"questions":[]}' + } + }) + expect( + server.ingestAntigravityScreenPermission({ baseline: baseline(server), command: null }) + ).toBe(false) + expect(baseline(server).state).toBe('waiting') + }) + + it('coalesces an unchanged permission observation without refreshing its revision', () => { + const server = workingPane() + server.ingestAntigravityScreenPermission({ baseline: baseline(server), command: 'echo OK' }) + const waiting = baseline(server) + expect( + server.ingestAntigravityScreenPermission({ baseline: waiting, command: 'echo OK' }) + ).toBe(true) + expect(baseline(server).observation).toEqual(waiting.observation) + }) +}) diff --git a/src/main/agent-hooks/server/server-ingest-antigravity-screen.ts b/src/main/agent-hooks/server/server-ingest-antigravity-screen.ts new file mode 100644 index 00000000000..8a595e3f191 --- /dev/null +++ b/src/main/agent-hooks/server/server-ingest-antigravity-screen.ts @@ -0,0 +1,78 @@ +import type { AgentStatusIpcPayload } from '../../../shared/agent-status-types' +import type { EnrichedAgentHookEventPayload } from './server-types' +import { AgentHookServerIngestNormalization } from './server-ingest-normalization' + +export type AntigravityScreenPermissionObservation = { + baseline: Pick + command: string | null +} + +const SCREEN_APPROVAL_PREFIX = '{"approval":{"source":"antigravity-screen",' + +export abstract class AgentHookServerIngestAntigravityScreen extends AgentHookServerIngestNormalization { + ingestAntigravityScreenPermission(request: AntigravityScreenPermissionObservation): boolean { + const { baseline, command } = request + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This server's canonical row writer stores enriched events in the shared listener map. + const previous = this.state.lastStatusByPaneKey.get(baseline.paneKey) as + | EnrichedAgentHookEventPayload + | undefined + const expected = baseline.observation + const current = previous?.observation + if ( + !previous || + previous.restoredUnconfirmed || + previous.connectionId || + previous.payload.agentType !== 'antigravity' || + !baseline.terminalHandle || + previous.terminalHandle !== baseline.terminalHandle || + !expected || + !current || + current.authorityId !== expected.authorityId || + current.incarnation !== expected.incarnation || + current.revision !== expected.revision || + this.getAgentStatusDisposition(baseline.paneKey) !== 'accept' + ) { + return false + } + const ownsPrompt = + current.origin === 'process' && + previous.payload.state === 'waiting' && + previous.payload.interactivePrompt?.startsWith(SCREEN_APPROVAL_PREFIX) === true + if (command === null ? !ownsPrompt : previous.payload.state !== 'working' && !ownsPrompt) { + return false + } + const interactivePrompt = + command === null + ? undefined + : JSON.stringify({ + approval: { + source: 'antigravity-screen', + tool: previous.payload.toolName ?? 'run_command', + summary: command + } + }) + if (interactivePrompt === previous.payload.interactivePrompt) { + return true + } + // A cleared dialog proves only that permission ended; completion still comes from the provider. + return ( + this.applyNormalizedStatus( + { + paneKey: previous.paneKey, + tabId: previous.tabId, + worktreeId: previous.worktreeId, + connectionId: previous.connectionId, + terminalHandle: previous.terminalHandle, + providerSession: previous.providerSession, + payload: { + ...previous.payload, + state: command === null ? 'working' : 'waiting', + interactivePrompt + } + }, + undefined, + 'process' + ) !== undefined + ) + } +} diff --git a/src/main/agent-hooks/server/server-ingest-terminal.ts b/src/main/agent-hooks/server/server-ingest-terminal.ts index e7e68115659..a927e8fc037 100644 --- a/src/main/agent-hooks/server/server-ingest-terminal.ts +++ b/src/main/agent-hooks/server/server-ingest-terminal.ts @@ -4,9 +4,9 @@ import { parseLegacyNumericPaneKey, parsePaneKey } from '../../../shared/stable- import { terminalStatusPayloadMatchesHook } from '../../../shared/agent-terminal-status-equivalence' import type { ParsedAgentStatusPayload } from '../../../shared/agent-status-types' import type { EnrichedAgentHookEventPayload } from './server-types' -import { AgentHookServerIngestNormalization } from './server-ingest-normalization' +import { AgentHookServerIngestAntigravityScreen } from './server-ingest-antigravity-screen' -export abstract class AgentHookServerIngestTerminal extends AgentHookServerIngestNormalization { +export abstract class AgentHookServerIngestTerminal extends AgentHookServerIngestAntigravityScreen { ingestTerminalStatus(event: { ptyId?: string paneKey: string diff --git a/src/main/orcad/orcad-entry.ts b/src/main/orcad/orcad-entry.ts index fb22f92c392..6d9f2c6b28a 100644 --- a/src/main/orcad/orcad-entry.ts +++ b/src/main/orcad/orcad-entry.ts @@ -233,6 +233,7 @@ async function startOrcadRuntime( // PTY agent on this host, and the store is the only place `worktree.ps` and the mobile // projection read from — unwired, orcad lists no PTY agents at all. onTerminalAgentStatus: (event) => agentHookServer.ingestTerminalStatus(event), + onTerminalScreenPermission: (event) => agentHookServer.ingestAntigravityScreenPermission(event), // Why here too and not only on the desktop: orcad serves `worktree.ps` and `agentSession.*`, // so without these a headless host publishes its structured chats nowhere and lists no agents. getAgentStatusSnapshot: () => diff --git a/src/main/runtime/__fixtures__/antigravity-windows-command-allow-key.meta.json b/src/main/runtime/__fixtures__/antigravity-windows-command-allow-key.meta.json new file mode 100644 index 00000000000..d4a019e1673 --- /dev/null +++ b/src/main/runtime/__fixtures__/antigravity-windows-command-allow-key.meta.json @@ -0,0 +1,9 @@ +{ + "capturedAt": "2026-09-20T09:26:40.702Z", + "platform": "win32", + "command": ["C:\\Users\\user\\AppData\\Local\\agy\\bin\\agy.exe"], + "cols": 120, + "rows": 40, + "note": "Native Windows agy 1.2.7; test one-time approval key 1; 120x40.", + "exitCode": 1 +} diff --git a/src/main/runtime/__fixtures__/antigravity-windows-command-allow-key.txt b/src/main/runtime/__fixtures__/antigravity-windows-command-allow-key.txt new file mode 100644 index 00000000000..48996981f23 --- /dev/null +++ b/src/main/runtime/__fixtures__/antigravity-windows-command-allow-key.txt @@ -0,0 +1,209 @@ +[?9001h[?1004h[?25l[>4m]0;C:\Users\user\AppData\Local\agy\bin\agy.exe[?25h[?1049h[?2004h[>4;2m[>1u[?u + ▄▀▀▄ + ▀▀▀▀▀▀ + ▀▀▀▀▀▀▀▀ + ▄▀▀ ▀▀▄ + ▄▀▀ ▀▀▄ + + Welcome to the Antigravity CLI. You are currently not signed in. + + No authentication methods available. + + Press ctrl+c or ctrl+d twice to exit. + + + + + + + + + + + + + + + + + + + + + + + + + + + +[?25l[>4m[<1u[?1049l[>4;2m[>1u[?u[0 q + ▄▀▀▄ Antigravity CLI 1.2.7 + ▀▀▀▀▀▀ Gemini 3.1 Pro (Low) + ▀▀▀▀▀▀▀▀ C:/orca-agy-verify-0920 + ▄▀▀ ▀▀▄ + ▄▀▀ ▀▀▄ + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +? for shortcuts Gemini 3.1 Pro · low + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[?25h[?25l + ▄▀▀▄ Antigravity CLI 1.2.7 + ▀▀▀▀▀▀ Gemini 3.1 Pro (Low) + ▀▀▀▀▀▀▀▀ C:/orca-agy-verify-0920 + ▄▀▀ ▀▀▄ + ▄▀▀ ▀▀▄ + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +? for shortcuts Gemini 3.1 Pro · low + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[?25h[?25lUsG[?25he the terminal tool to printORCA_PERMISSION_ALLOW_OK.Donot modify files.[?25l[?25h[?25l? for shortcuts[?25h[?25l +> Use the terminal tool to print ORCA_PERMISSION_ALLOW_OK. Do not modify files. +⣾ Generating... +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low[?25h[?25l⣷ [?25h[?25l⣯ Gene[?25h[?25l⣟ Genera[?25h[?25l⡿ Generat[?25h[?25l⢿ Generati[?25h[?25l⣻ Generating[?25h[?25l⣽ Generating.[?25h[?25l⣾ Generating..[?25h[?25l⣷ Generating...[?25h[?25l⣯ Generating[?25h[?25l⣟ Generating.[?25h[?25l⡿ Generating..[?25h[?25l⢿ Generating...[?25h[?25l⣻ [?25h[?25l⣽ [?25h[?25l⣾ [?25h[?25l⣷ [?25h[?25l⣯ [?25h[?25l⣟ Ge[?25h[?25l⡿ Gen[?25h[?25ler[?25h[?25l⢿ [?25h[?25l⣻ Gener[?25h[?25l⣽ Generat[?25h[?25l⣾ Generati[?25h[?25l⣷ Generatin[?25h[?25l⣯ Generating[?25h[?25l⣟ Generating..[?25h[?25l⡿ Generating...[?25h[?25l⢿ Generatin[?25h[?25l I'm focusing intently on tool selection, always opting for the most specialized option available. I'm actively avo... +└ Tip: Use /skills to browse and manage agent skills. +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low[?25h[?25l⣻ I'm focusing intently on tool selection, always opting for the most specialized option avail[?25h[?25ln avai[?25h[?25l⣽ I'm focusing intently on tool selection, always opting for the most specialized option ava[?25h[?25lion av[?25h[?25ltion a[?25h[?25l⣾ I'm focusing intently on tool selection, always opting for the most specialized option [?25h[?25loption[?25h[?25l optio[?25h[?25l⣷[?25h[?25ld opti[?25h[?25led opt[?25h[?25l⣯ I'm focusing intently on tool selection, always opting for the most specialized op[?25h[?25lized o[?25h[?25l⣟ I'm focusing intently on tool selection, always opting for the most specialized [?25h[?25lalized[?25h[?25lialize[?25h[?25lcializ[?25h[?25l⡿[?25h[?25leciali[?25h[?25lpecial[?25h[?25l⢿[?25h[?25lspecia[?25h[?25l speci[?25h[?25l⣻ I'm focusing intently on tool selection, always opting for the most spec[?25h[?25lst spe[?25h[?25l +▸ Thought for 3s, 246 tokens + I'm focusing intently on tool selection, always opting for the most specialized option available. I'm actively avo...  + +● Bash(Write-Host "ORCA_PERMISSION_ALLOW_OK") (ctrl+o to expand) +⣻ Running command... +└ Tip: Use /skills to browse and manage agent skills. +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low[?25h[0 q[?25l +Command +Requesting permission for: + Write-Host "ORCA_PERMISSION_ALLOW_OK" + +Run this command? +> 1. Yes, run command + 2. Yes, and always allow in this conversation for commands that start with 'Write-Host "ORCA_PERMISSION_ALLOW_OK"' + 3. Yes, and always allow for commands that start with 'Write-Host "ORCA_PERMISSION_ALLOW_OK"' (Persist to +settings.json) + 4. No, cancel + + ↑/↓ Navigate · tab Amend · ctrl+g edit/expand command +esc to cancel Gemini 3.1 Pro · low[0 q○  +⣽ Running command... +└ Tip: Use /diff to view uncommitted changes in your workspace.> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low + + + + + + + + + + + + + + + + + + + +[?25h[?25l⣾ [?25h[?25l⣷ Running command[?25h[?25l⣯ Running command.[?25h[?25l● ..[?25h[?25l● [?25h[?25l⣟ Running command...[?25h[?25l⡿ [?25h[?25l⢿ [?25h[?25l⣻ [?25h[?25l⣽ [?25h[?25l⣾ [?25h[?25l⣷ R[?25h[?25lun[?25h[?25l⣯ [?25h[?25l⣟ Run[?25h[?25l⡿ Runni[?25h[?25l⢿ Runnin[?25h[?25l⣻ Running[?25h[?25l⣽ Running c[?25h[?25l⣾ Running co[?25h[?25l⣷ Running com[?25h[?25l⣯ Running comm[?25h[?25l⣟ Running comman[?25h[?25lommand[?25h[?25l⡿ [?25h[?25l⢿ Running command.[?25h[?25l⣻ Running command..[?25h[?25l⣽ Running command...[?25h[?25l⣾ Running command[?25h[?25l⣷ Running command.[?25h[?25l⣯ Running command...[?25h[?25l⣟ [?25h[?25l⡿ [?25h[?25l⢿ [?25h[?25l + I have executed the comm +⢿ Running command... +└ Tip: Use /diff to view uncommitted changes in your workspace.> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low[?25h[?25land in the t[?25h[?25lerminal,[?25h[?25l⣻ [?25h[?25land it successfully printed ORCA_PERMISSION_ALLOW_OK. Let me know if + you need anything else! +⣻ Running command... +└ Tip: Use /diff to view uncommitted changes in your workspace. +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low[?25h[?25l⣽ [?25h[?25l +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +? for shortcuts Gemini 3.1 Pro · low + + + + + + + + + + + + + + + + +[?25h \ No newline at end of file diff --git a/src/main/runtime/__fixtures__/antigravity-windows-command-approval.meta.json b/src/main/runtime/__fixtures__/antigravity-windows-command-approval.meta.json new file mode 100644 index 00000000000..9cf6c09e06e --- /dev/null +++ b/src/main/runtime/__fixtures__/antigravity-windows-command-approval.meta.json @@ -0,0 +1,9 @@ +{ + "capturedAt": "2026-09-20T09:20:25.623Z", + "platform": "win32", + "command": ["C:\\Users\\user\\AppData\\Local\\agy\\bin\\agy.exe"], + "cols": 120, + "rows": 40, + "note": "Native Windows agy 1.2.7; authenticated profile; harmless command approval; 120x40.", + "exitCode": 1 +} diff --git a/src/main/runtime/__fixtures__/antigravity-windows-command-approval.txt b/src/main/runtime/__fixtures__/antigravity-windows-command-approval.txt new file mode 100644 index 00000000000..7b701d36ccc --- /dev/null +++ b/src/main/runtime/__fixtures__/antigravity-windows-command-approval.txt @@ -0,0 +1,148 @@ +[?9001h[?1004h[?25l[>4m]0;C:\Users\user\AppData\Local\agy\bin\agy.exe[?25h[?1049h[?2004h[>4;2m[>1u[?u + ▄▀▀▄ + ▀▀▀▀▀▀ + ▀▀▀▀▀▀▀▀ + ▄▀▀ ▀▀▄ + ▄▀▀ ▀▀▄ + + Welcome to the Antigravity CLI. You are currently not signed in. + + No authentication methods available. + + Press ctrl+c or ctrl+d twice to exit. + + + + + + + + + + + + + + + + + + + + + + + + + + + +[?25l[>4m[<1u[?1049l[>4;2m[>1u[?u[0 q + ▄▀▀▄ Antigravity CLI 1.2.7 + ▀▀▀▀▀▀ Gemini 3.1 Pro (Low) + ▀▀▀▀▀▀▀▀ C:/orca-agy-verify-0920 + ▄▀▀ ▀▀▄ + ▄▀▀ ▀▀▄ + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +? for shortcuts Gemini 3.1 Pro · low + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[?25h[?25l + ▄▀▀▄ Antigravity CLI 1.2.7 + ▀▀▀▀▀▀ Gemini 3.1 Pro (Low) + ▀▀▀▀▀▀▀▀ C:/orca-agy-verify-0920 + ▄▀▀ ▀▀▄ + ▄▀▀ ▀▀▄ + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +? for shortcuts Gemini 3.1 Pro · low + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[?25h[?25lUseG[?25h theterminal tool to print ORCA_PERMISSION_CAPTURE_OK.Do not modify files.[?25l[?25h[?25l? for shortcuts[?25h[?25l +> Use the terminal tool to print ORCA_PERMISSION_CAPTURE_OK. Do not modify files. + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +? for shortcuts Gemini 3.1 Pro · low[?25h[?25l⣾ Generating...esc to cancel [?25h[?25l⣷ [?25h[?25l⣯ [?25h[?25l⣟ [?25h[?25l⡿ [?25h[?25l⢿ [?25h[?25l⣻ Ge[?25h[?25l⣽ Gen[?25h[?25l⣾ Gene[?25h[?25l⣷ Gener[?25h[?25l⣯ Generat[?25h[?25l⣟ Generati[?25h[?25l⡿ Generatin[?25h[?25l⢿ Generating[?25h[?25l⣻ Generating.[?25h[?25l⣽ Generating...[?25h[?25l⣾ Generatin[?25h[?25l⣷ Generating[?25h[?25l⣯ Generating.[?25h[?25l⣟ Generating...[?25h[?25l⡿ [?25h[?25l⢿ [?25h[?25l⣻ [?25h[?25l⣽ [?25h[?25l⣾ [?25h[?25l⣷ G[?25h[?25l⣯ Ge[?25h[?25l⣟ Gene[?25h[?25l⡿ Gener[?25h[?25l⢿ Genera[?25h[?25l⣻ Generat[?25h[?25lneratin[?25h[?25l⣽ [?25h[?25l I've zeroed in on the `run_command` tool. My goal is to use it to print `ORCA_PERMISSION_CAPTURE_OK` without alter...[?25h[?25l⣾ I've zeroed in on the `run_command` tool. My goal is to use it to[?25h[?25le it t[?25h[?25lse it [?25h[?25l⣷[?25h[?25luse it[?25h[?25l use i[?25h[?25lo use [?25h[?25l⣯[?25h[?25lto use[?25h[?25l to us[?25h[?25l⣟[?25h[?25ls to u[?25h[?25lis to [?25h[?25l⡿ I've zeroed in on the `run_command` tool. My goal is to[?25h[?25ll is t[?25h[?25lal is [?25h[?25l⢿[?25h[?25loal is[?25h[?25lgoal i[?25h[?25l goal [?25h[?25l⣻[?25h[?25ly goal[?25h[?25lMy goa[?25h[?25l⣽ I've zeroed in on the `run_command` tool. My go[?25h[?25l. My g[?25h[?25l⣾ I've zeroed in on the `run_command` tool. My [?25h[?25lol. My[?25h[?25lool. M[?25h[?25l⣷[?25h[?25ltool. [?25h[?25l tool.[?25h[?25l⣯ I've zeroed in on the `run_command` tool[?25h[?25ld` too[?25h[?25lnd` to[?25h[?25l⣟[?25h[?25land` t[?25h[?25lmand` [?25h[?25l⡿[?25h[?25lmmand`[?25h[?25lommand[?25h[?25l⢿ I've zeroed in on the `run_comman[?25h[?25l_comma[?25h[?25ln_comm[?25h[?25l⣻[?25h[?25lun_com[?25h[?25lrun_co[?25h[?25l`run_c[?25h[?25l⣽[?25h[?25l `run_[?25h[?25le `run[?25h[?25lhe `ru[?25h[?25l +▸ Thought for 4s, 310 tokens + I've zeroed in on the `run_command` tool. My goal is to use it to print `ORCA_PERMISSION_CAPTURE_OK` without alter... + +○ Bash(echo "ORCA_PERMISSION_CAPTURE_OK") (ctrl+o to expand) +⣾ Running command... +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low[?25h[0 q[?25l●  + +Command +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +Requesting permission for: + echo "ORCA_PERMISSION_CAPTURE_OK" + +Run this command? +> 1. Yes, run command + 2. Yes, and always allow in this conversation for commands that start with 'echo "ORCA_PERMISSION_CAPTURE_OK"' + 3. Yes, and always allow for commands that start with 'echo "ORCA_PERMISSION_CAPTURE_OK"' (Persist to settings.json) + 4. No, cancel + + ↑/↓ Navigate · tab Amend · ctrl+g edit/expand command +esc to cancel Gemini 3.1 Pro · low \ No newline at end of file diff --git a/src/main/runtime/__fixtures__/antigravity-windows-command-cancelled.meta.json b/src/main/runtime/__fixtures__/antigravity-windows-command-cancelled.meta.json new file mode 100644 index 00000000000..ce03cc4c86f --- /dev/null +++ b/src/main/runtime/__fixtures__/antigravity-windows-command-cancelled.meta.json @@ -0,0 +1,9 @@ +{ + "capturedAt": "2026-09-20T09:23:21.869Z", + "platform": "win32", + "command": ["C:\\Users\\user\\AppData\\Local\\agy\\bin\\agy.exe"], + "cols": 120, + "rows": 40, + "note": "Native Windows agy 1.2.7; command approval dismissed with Escape; 120x40.", + "exitCode": 1 +} diff --git a/src/main/runtime/__fixtures__/antigravity-windows-command-cancelled.txt b/src/main/runtime/__fixtures__/antigravity-windows-command-cancelled.txt new file mode 100644 index 00000000000..55aa2d5a343 --- /dev/null +++ b/src/main/runtime/__fixtures__/antigravity-windows-command-cancelled.txt @@ -0,0 +1,173 @@ +[?9001h[?1004h[?25l[>4m]0;C:\Users\user\AppData\Local\agy\bin\agy.exe[?25h[?1049h[?2004h[>4;2m[>1u[?u + ▄▀▀▄ + ▀▀▀▀▀▀ + ▀▀▀▀▀▀▀▀ + ▄▀▀ ▀▀▄ + ▄▀▀ ▀▀▄ + + Welcome to the Antigravity CLI. You are currently not signed in. + + No authentication methods available. + + Press ctrl+c or ctrl+d twice to exit. + + + + + + + + + + + + + + + + + + + + + + + + + + + +[?25l[>4m[<1u[?1049l[>4;2m[>1u[?u[0 q + ▄▀▀▄ Antigravity CLI 1.2.7 + ▀▀▀▀▀▀ Gemini 3.1 Pro (Low) + ▀▀▀▀▀▀▀▀ C:/orca-agy-verify-0920 + ▄▀▀ ▀▀▄ + ▄▀▀ ▀▀▄ + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +? for shortcuts Gemini 3.1 Pro · low + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[?25h[?25l + ▄▀▀▄ Antigravity CLI 1.2.7 + ▀▀▀▀▀▀ Gemini 3.1 Pro (Low) + ▀▀▀▀▀▀▀▀ C:/orca-agy-verify-0920 + ▄▀▀ ▀▀▄ + ▄▀▀ ▀▀▄ + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +? for shortcuts Gemini 3.1 Pro · low + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[?25hUs[?25leG[?25htheterminal tool toprint ORCA_PERMISSION_CANCEL_OK. Do not modify files.[?25l? for shortcuts[?25h[?25l +> Use the terminal tool to print ORCA_PERMISSION_CANCEL_OK. Do not modify files. +⣾ Generating... +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low[?25h[?25l⣷ [?25h[?25l⣯ Generating[?25h[?25l⣟ Generating..[?25h[?25l⡿ Generating...[?25h[?25l⢿ [?25h[?25l⣻ [?25h[?25l⣽ [?25h[?25l⣾ [?25h[?25l⣷ [?25h[?25l⣯ G[?25h[?25l⣟ Ge[?25h[?25lne[?25h[?25l⡿ Gene[?25h[?25l⢿ Gener[?25h[?25l⣻ Genera[?25h[?25l⣽ Generat[?25h[?25l⣾ Generatin[?25h[?25l⣷ Generating[?25h[?25l⣯ Generating.[?25h[?25l⣟ Generating..[?25h[?25l⡿ Generating...[?25h[?25l⢿ Generating[?25h[?25l⣻ Generating.[?25h[?25l⣽ Generating..[?25h[?25l⣾ Generating...[?25h[?25l⣷ [?25h[?25l⣯ [?25h[?25l⣟ [?25h[?25l⡿ [?25h[?25l⢿ G[?25h[?25l⣻ Ge[?25h[?25lne[?25h[?25l⣽ I'm focusing intently on tool specificity. I've been refining my approach to avoid unnecessary use of `cat` within... +└ Tip: Use /settings to configure your environment. +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low[?25h[?25lng int[?25h[?25l⣾ I'm focusing inte[?25h[?25l inten[?25h[?25lintent[?25h[?25l⣷[?25h[?25lntentl[?25h[?25ltently[?25h[?25lently [?25h[?25l⣯[?25h[?25lntly o[?25h[?25ltly on[?25h[?25l⣟ I'm focusing intently on [?25h[?25ly on t[?25h[?25l⡿ I'm focusing intently on to[?25h[?25lon too[?25h[?25ln tool[?25h[?25l⢿ I'm focusing intently on tool [?25h[?25ltool s[?25h[?25lool sp[?25h[?25l⣻[?25h[?25lol spe[?25h[?25ll spec[?25h[?25l⣽ I'm focusing intently on tool speci[?25h[?25lspecif[?25h[?25lpecifi[?25h[?25l⣾[?25h[?25lecific[?25h[?25lcifici[?25h[?25l⣷[?25h[?25lificit[?25h[?25lficity[?25h[?25licity.[?25h[?25l⣯[?25h[?25lcity. [?25h[?25lity. I[?25h[?25l⣟ I'm focusing intently on tool specificity. I'[?25h[?25ly. I'v[?25h[?25l⡿ I'm focusing intently on tool specificity. I've[?25h[?25l I've [?25h[?25lI've b[?25h[?25l⢿ I'm focusing intently on tool specificity. I've be[?25h[?25lve bee[?25h[?25l +▸ Thought for 4s, 365 tokens + I'm focusing intently on tool specificity. I've been refining my approach to avoid unnecessary use of `cat` within...  + +● Bash(echo ORCA_PERMISSION_CANCEL_OK) (ctrl+o to expand) +⢿ Running command... +└ Tip: Use /settings to configure your environment. +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low[?25h[0 q[?25l +Command +Requesting permission for: + echo ORCA_PERMISSION_CANCEL_OK + +Run this command? +> 1. Yes, run command + 2. Yes, and always allow in this conversation for commands that start with 'echo ORCA_PERMISSION_CANCEL_OK' + 3. Yes, and always allow for commands that start with 'echo ORCA_PERMISSION_CANCEL_OK' (Persist to settings.json) + 4. No, cancel + + ↑/↓ Navigate · tab Amend · ctrl+g edit/expand command +esc to cancel Gemini 3.1 Pro · low[0 q●  ⎿ Interrupted · What should Antigravity CLI do instead?> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +? for shortcuts Gemini 3.1 Pro · low + + + + + + + + + + + + + + + + + + + +[?25h \ No newline at end of file diff --git a/src/main/runtime/agent-status-store-wiring.test-fixture.ts b/src/main/runtime/agent-status-store-wiring.test-fixture.ts index 1f399e0464f..bd45c4bf3ef 100644 --- a/src/main/runtime/agent-status-store-wiring.test-fixture.ts +++ b/src/main/runtime/agent-status-store-wiring.test-fixture.ts @@ -20,6 +20,9 @@ export function makeAgentStatusStoreWiring(): { statusStore: AgentHookServer deps: { onTerminalAgentStatus: (event: Parameters[0]) => void + onTerminalScreenPermission: ( + event: Parameters[0] + ) => boolean getAgentStatusSnapshot: () => ReturnType getAgentProviderSessionSnapshot: () => ReturnType getAgentProviderSessionRowsForPane: ( @@ -37,6 +40,7 @@ export function makeAgentStatusStoreWiring(): { statusStore, deps: { onTerminalAgentStatus: (event) => statusStore.ingestTerminalStatus(event), + onTerminalScreenPermission: (event) => statusStore.ingestAntigravityScreenPermission(event), getAgentStatusSnapshot: () => statusStore.getStatusSnapshot().filter((entry) => entry.providerSessionOnly !== true), getAgentProviderSessionSnapshot: () => statusStore.getStatusSnapshot(), diff --git a/src/main/runtime/antigravity-command-approval-screen.ts b/src/main/runtime/antigravity-command-approval-screen.ts new file mode 100644 index 00000000000..f51ed024e7b --- /dev/null +++ b/src/main/runtime/antigravity-command-approval-screen.ts @@ -0,0 +1,23 @@ +/** Matches the captured agy command dialog only while it owns the bottom of the screen. */ +export function isAntigravityCommandApprovalScreen(rows: readonly string[]): boolean { + const lines = rows.map((row) => row.trim()).filter(Boolean) + const footer = lines.at(-1) ?? '' + const navigation = lines.at(-2) ?? '' + if ( + !footer.startsWith('esc to cancel') || + navigation !== '↑/↓ Navigate · tab Amend · ctrl+g edit/expand command' + ) { + return false + } + const question = lines.lastIndexOf('Run this command?') + if (question === -1) { + return false + } + const choices = lines.slice(question + 1, -2).map((line) => line.replace(/^>\s*/, '')) + return ( + choices[0] === '1. Yes, run command' && + choices.some((line) => line.startsWith('2. Yes, and always allow in this conversation')) && + choices.some((line) => line.startsWith('3. Yes, and always allow for commands')) && + choices.at(-1) === '4. No, cancel' + ) +} diff --git a/src/main/runtime/antigravity-command-approval-transcript.test.ts b/src/main/runtime/antigravity-command-approval-transcript.test.ts new file mode 100644 index 00000000000..bb107a45734 --- /dev/null +++ b/src/main/runtime/antigravity-command-approval-transcript.test.ts @@ -0,0 +1,94 @@ +import { makeAgentStatusStoreWiring } from './agent-status-store-wiring.test-fixture' +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it, vi } from 'vitest' +import { createTranscriptPane, TRANSCRIPT_PANE_PTY_ID } from './agent-transcript-pane-test-harness' +import { extractLastOscTitle } from '../../shared/osc-title-extraction' + +vi.mock('electron', () => ({ + BrowserWindow: { fromId: vi.fn(() => null) }, + webContents: { fromId: vi.fn(() => null) }, + ipcMain: { on: vi.fn(), removeListener: vi.fn() }, + app: { getPath: vi.fn(() => '/tmp') } +})) + +describe('captured native Windows Antigravity command approval', () => { + it.each([ + ['approval', false, 'agent-approval-prompt'], + ['cancelled', true, undefined], + ['allow-key', true, undefined] + ] as const)( + 'classifies the recorded %s screen', + async (name, satisfied, blockedReason) => { + const transcript = readFileSync( + join(__dirname, `__fixtures__/antigravity-windows-command-${name}.txt`), + 'utf8' + ) + const { runtime, handle } = await createTranscriptPane({ + paneTitle: extractLastOscTitle(transcript) ?? 'agy', + foregroundProcess: 'agy', + size: { cols: 120, rows: 40 }, + data: transcript + }) + const result = await runtime.waitForTerminal(handle, { + condition: 'tui-idle', + timeoutMs: 3500 + }) + expect(result.satisfied).toBe(satisfied) + if (!satisfied) { + expect(result).toMatchObject({ blockedReason }) + } + }, + 10000 + ) +}) + +it('publishes the captured permission and cancellation to the canonical hook store', async () => { + const wiring = makeAgentStatusStoreWiring() + const { runtime, handle } = await createTranscriptPane( + { + paneTitle: 'agy', + foregroundProcess: 'agy', + size: { cols: 120, rows: 40 }, + data: '' + }, + wiring.deps + ) + const paneKey = runtime.getTerminalPaneKey(handle) + if (!paneKey) { + throw new Error('Missing test pane key') + } + wiring.statusStore.ingestTerminalStatus({ + paneKey, + terminalHandle: handle, + payload: { + state: 'working', + agentType: 'antigravity', + prompt: 'print marker', + toolName: 'run_command' + } + }) + const approval = readFileSync( + join(__dirname, '__fixtures__/antigravity-windows-command-approval.txt'), + 'utf8' + ) + runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, approval, Date.now()) + await vi.waitFor(() => + expect(wiring.statusStore.getStatusSnapshot()[0]).toMatchObject({ + state: 'waiting', + observation: { origin: 'process' } + }) + ) + expect(wiring.statusStore.getStatusSnapshot()[0].interactivePrompt).toContain( + 'ORCA_PERMISSION_CAPTURE_OK' + ) + const cancelled = readFileSync( + join(__dirname, '__fixtures__/antigravity-windows-command-cancelled.txt'), + 'utf8' + ) + runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, cancelled, Date.now()) + await vi.waitFor(() => + expect(wiring.statusStore.getStatusSnapshot()[0]).toMatchObject({ state: 'working' }) + ) + expect(wiring.statusStore.getStatusSnapshot()[0].interactivePrompt).toBeUndefined() +}) diff --git a/src/main/runtime/antigravity-screen-permission-publisher.test.ts b/src/main/runtime/antigravity-screen-permission-publisher.test.ts new file mode 100644 index 00000000000..d0db7dc9ac6 --- /dev/null +++ b/src/main/runtime/antigravity-screen-permission-publisher.test.ts @@ -0,0 +1,110 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { Terminal } from '@xterm/headless' +import { beforeAll, describe, expect, it, vi } from 'vitest' +import type { AgentStatusIpcPayload } from '../../shared/agent-status-types' +import type { RuntimeVisibleTerminalState } from './runtime-terminal-state-records' +import { AntigravityScreenPermissionPublisher } from './antigravity-screen-permission-publisher' + +const baseline: AgentStatusIpcPayload = { + paneKey: 'pane', + terminalHandle: 'term-1', + connectionId: null, + agentType: 'antigravity', + state: 'working', + prompt: 'print marker', + receivedAt: 1, + stateStartedAt: 1 +} +let approval: RuntimeVisibleTerminalState + +beforeAll(async () => { + const terminal = new Terminal({ cols: 120, rows: 40, allowProposedApi: true }) + try { + const raw = readFileSync( + join(__dirname, '__fixtures__/antigravity-windows-command-approval.txt'), + 'utf8' + ) + await new Promise((resolve) => terminal.write(raw, resolve)) + const buffer = terminal.buffer.active + approval = { + lines: Array.from( + { length: 40 }, + (_, row) => buffer.getLine(buffer.baseY + row)?.translateToString(true) ?? '' + ), + generation: 1, + sequence: 1, + isAlternateScreen: true + } + } finally { + terminal.dispose() + } +}) + +describe('Antigravity screen publication scheduling', () => { + it('coalesces output during a read and retries the newest frame', async () => { + const frame = Promise.withResolvers() + const readScreen = vi.fn().mockReturnValueOnce(frame.promise).mockResolvedValue(approval) + const publish = vi.fn(() => true) + const publisher = new AntigravityScreenPermissionPublisher({ + baseline: () => baseline, + readScreen, + isCurrent: () => true, + publish + }) + publisher.schedule('pty') + for (let index = 0; index < 100; index += 1) { + publisher.schedule('pty') + } + expect(readScreen).toHaveBeenCalledTimes(1) + frame.resolve(null) + await vi.waitFor(() => expect(publish).toHaveBeenCalledTimes(1)) + expect(readScreen).toHaveBeenCalledTimes(2) + expect(publish).toHaveBeenCalledWith({ baseline, command: 'echo "ORCA_PERMISSION_CAPTURE_OK"' }) + }) + + it('does not publish a frame rejected by generation, sequence or liveness validation', async () => { + const isCurrent = vi.fn(() => false) + const publish = vi.fn(() => true) + const publisher = new AntigravityScreenPermissionPublisher({ + baseline: () => baseline, + readScreen: async () => approval, + isCurrent, + publish + }) + publisher.schedule('pty') + await vi.waitFor(() => expect(isCurrent).toHaveBeenCalled()) + expect(publish).not.toHaveBeenCalled() + }) + + it('does not interpret another provider or a direct SSH mirror', async () => { + const readScreen = vi.fn(async () => approval) + const publisher = new AntigravityScreenPermissionPublisher({ + baseline: (id) => + id === 'ssh' + ? { ...baseline, connectionId: 'remote' } + : { ...baseline, agentType: 'claude' }, + readScreen, + isCurrent: () => true, + publish: () => true + }) + publisher.schedule('ssh') + publisher.schedule('claude') + await Promise.resolve() + expect(readScreen).not.toHaveBeenCalled() + }) + + it('does not lose output queued between a synchronous empty read and its cleanup', async () => { + const readBaseline = vi.fn().mockReturnValueOnce(null).mockReturnValue(baseline) + const publish = vi.fn(() => true) + const publisher = new AntigravityScreenPermissionPublisher({ + baseline: readBaseline, + readScreen: async () => approval, + isCurrent: () => true, + publish + }) + publisher.schedule('pty') + publisher.schedule('pty') + await vi.waitFor(() => expect(publish).toHaveBeenCalledTimes(1)) + }) +}) diff --git a/src/main/runtime/antigravity-screen-permission-publisher.ts b/src/main/runtime/antigravity-screen-permission-publisher.ts new file mode 100644 index 00000000000..1e9f23f7163 --- /dev/null +++ b/src/main/runtime/antigravity-screen-permission-publisher.ts @@ -0,0 +1,68 @@ +import type { AgentStatusIpcPayload } from '../../shared/agent-status-types' +import type { AntigravityScreenPermissionObservation } from '../agent-hooks/server/server-ingest-antigravity-screen' +import type { RuntimeVisibleTerminalState } from './runtime-terminal-state-records' +import { isAntigravityCommandApprovalScreen } from './antigravity-command-approval-screen' +import { isKnownReadyTerminalScreen } from './terminal-screen-readiness' + +type Dependencies = { + baseline(ptyId: string): AgentStatusIpcPayload | null + readScreen(ptyId: string): Promise + isCurrent(ptyId: string, screen: RuntimeVisibleTerminalState): boolean + publish(observation: AntigravityScreenPermissionObservation): boolean +} + +export class AntigravityScreenPermissionPublisher { + private readonly pending = new Map() + + constructor(private readonly deps: Dependencies) {} + + schedule(ptyId: string): void { + const existing = this.pending.get(ptyId) + if (existing) { + existing.dirty = true + return + } + const pending = { dirty: true } + this.pending.set(ptyId, pending) + void this.drain(ptyId, pending).finally(() => { + this.pending.delete(ptyId) + if (pending.dirty) { + this.schedule(ptyId) + } + }) + } + + private async drain(ptyId: string, pending: { dirty: boolean }): Promise { + while (pending.dirty) { + pending.dirty = false + const baseline = this.deps.baseline(ptyId) + if (!baseline || baseline.agentType !== 'antigravity' || baseline.connectionId) { + continue + } + try { + const screen = await this.deps.readScreen(ptyId) + if (!screen || !this.deps.isCurrent(ptyId, screen)) { + continue + } + if (isAntigravityCommandApprovalScreen(screen.lines)) { + const lines = screen.lines.map((line) => line.trim()) + const start = lines.lastIndexOf('Requesting permission for:') + const end = lines.lastIndexOf('Run this command?') + if (start !== -1 && end > start) { + const command = lines + .slice(start + 1, end) + .filter(Boolean) + .join('\n') + if (command) { + this.deps.publish({ baseline, command }) + } + } + } else if (isKnownReadyTerminalScreen({ tail: screen.lines, draft: screen.draft })) { + this.deps.publish({ baseline, command: null }) + } + } catch { + // An unreadable screen is not evidence that a permission prompt disappeared. + } + } + } +} diff --git a/src/main/runtime/orca-runtime-on-pty-data.ts b/src/main/runtime/orca-runtime-on-pty-data.ts index 160c99f2071..c780df49da2 100644 --- a/src/main/runtime/orca-runtime-on-pty-data.ts +++ b/src/main/runtime/orca-runtime-on-pty-data.ts @@ -66,6 +66,9 @@ export class OrcaRuntimeWithOnPtyData extends OrcaRuntimeWithPreparePtyExecution forwardQueryReplies ) captureModelReceipt?.(modelCompletion) + if (this.onTerminalScreenPermission) { + this.antigravityScreenPermissions.schedule(ptyId) + } const pty = this.getOrCreatePtyWorktreeRecord(ptyId) const ptyTailBefore = pty diff --git a/src/main/runtime/orca-runtime-preserved-branch-cleanup.ts b/src/main/runtime/orca-runtime-preserved-branch-cleanup.ts index 33b4ace7772..f489e69cc8a 100644 --- a/src/main/runtime/orca-runtime-preserved-branch-cleanup.ts +++ b/src/main/runtime/orca-runtime-preserved-branch-cleanup.ts @@ -1,4 +1,5 @@ // @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. +import type { AntigravityScreenPermissionObservation } from '../agent-hooks/server/server-ingest-antigravity-screen' import { OrcaRuntimeWithTerminalDrivers } from './orca-runtime-terminal-drivers' import { RuntimePreservedBranchCleanup } from './runtime-preserved-branch-cleanup' import type { IPtyProvider } from '../providers/types' @@ -55,6 +56,10 @@ export class OrcaRuntimeWithPreservedBranchCleanup extends OrcaRuntimeWithTermin protected readonly onPtyStopped: ((ptyId: string) => void) | null + protected readonly onTerminalScreenPermission: + | ((event: AntigravityScreenPermissionObservation) => boolean) + | null + protected readonly onTerminalAgentStatus: | ((event: RuntimeTerminalAgentStatusEvent) => void) | null diff --git a/src/main/runtime/orca-runtime-state-fields.ts b/src/main/runtime/orca-runtime-state-fields.ts index a28ead15724..04ed74ed509 100644 --- a/src/main/runtime/orca-runtime-state-fields.ts +++ b/src/main/runtime/orca-runtime-state-fields.ts @@ -1,4 +1,5 @@ // @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. +import type { AntigravityScreenPermissionObservation } from '../agent-hooks/server/server-ingest-antigravity-screen' import { OrcaRuntimeWithLinearCommands } from './orca-runtime-linear-commands' import type { RuntimeStore } from './runtime-store-contract' import type { StatsCollector } from '../stats/collector' @@ -52,6 +53,7 @@ export class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCommands { getSshProvider?: (connectionId: string) => IPtyProvider | undefined prepareClaudeAuth?: PrepareClaudeAuth onPtyStopped?: (ptyId: string) => void + onTerminalScreenPermission?: (event: AntigravityScreenPermissionObservation) => boolean onTerminalAgentStatus?: (event: RuntimeTerminalAgentStatusEvent) => void onTerminalSideEffects?: (batch: TerminalSideEffectBatch) => void // Why: agent status mostly arrives via hooks (agent-hooks/server), not OSC @@ -242,6 +244,7 @@ export class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCommands { this.getLocalProviderFn = deps?.getLocalProvider ?? null this.getSshProviderFn = deps?.getSshProvider ?? null this.onPtyStopped = deps?.onPtyStopped ?? null + this.onTerminalScreenPermission = deps?.onTerminalScreenPermission ?? null this.onTerminalAgentStatus = deps?.onTerminalAgentStatus ?? null this.buildAgentHookPtyEnv = deps?.buildAgentHookPtyEnv ?? null this.getDesktopWindowStatusFn = deps?.getDesktopWindowStatus ?? (() => 'openable') diff --git a/src/main/runtime/orca-runtime-visible-snapshot-preview.ts b/src/main/runtime/orca-runtime-visible-snapshot-preview.ts index 3c334285509..4a316377166 100644 --- a/src/main/runtime/orca-runtime-visible-snapshot-preview.ts +++ b/src/main/runtime/orca-runtime-visible-snapshot-preview.ts @@ -1,4 +1,5 @@ // @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. +import { AntigravityScreenPermissionPublisher } from './antigravity-screen-permission-publisher' import { OrcaRuntimeWithCaptureProviderTerminalBuffer } from './orca-runtime-capture-provider-terminal-buffer' import type { RuntimeTerminalProjection } from './orca-runtime-core' import { buildPreview } from './terminal-tail-state' @@ -16,6 +17,29 @@ import { import { withTimeout } from './runtime-async-boundaries' export class OrcaRuntimeWithVisibleSnapshotPreview extends OrcaRuntimeWithCaptureProviderTerminalBuffer { + protected readonly antigravityScreenPermissions = new AntigravityScreenPermissionPublisher({ + baseline: (ptyId) => { + const pty = this.ptysById.get(ptyId) + if (!pty?.connected || pty.connectionId || !pty.paneKey) { + return null + } + return ( + this.getAgentProviderSessionRowsForPaneFn?.(pty.paneKey)?.find( + (row) => row.providerSessionOnly !== true && row.agentType === 'antigravity' + ) ?? null + ) + }, + readScreen: (ptyId) => this.readVisibleTerminalState(ptyId), + isCurrent: (ptyId, screen) => + this.ptysById.get(ptyId)?.connected === true && + this.getPtyLivenessVerdict(ptyId)?.status !== 'unverifiable' && + screen.generation === this.getPtyLifecycleGeneration(ptyId) && + screen.sequence >= this.getPtyOutputSequence(ptyId) && + (!screen.headlessWriteChain || + screen.headlessWriteChain === this.headlessTerminals.get(ptyId)?.writeChain), + publish: (event) => this.onTerminalScreenPermission?.(event) ?? false + }) + protected getTerminalScreenReadiness( ptyId: string | null | undefined, retainedText: string diff --git a/src/main/runtime/terminal-screen-readiness.ts b/src/main/runtime/terminal-screen-readiness.ts index 30fc8fe56f2..ae73b70a88c 100644 --- a/src/main/runtime/terminal-screen-readiness.ts +++ b/src/main/runtime/terminal-screen-readiness.ts @@ -1,3 +1,4 @@ +import { isAntigravityCommandApprovalScreen } from './antigravity-command-approval-screen' import { detectTerminalWaitBlockedReason, isKnownReadyPromptPreview @@ -47,6 +48,10 @@ export function classifyTerminalScreenReadiness(screen: { const ready = isKnownReadyTerminalScreen(screen) return { ready, - blockedReason: ready ? null : detectTerminalWaitBlockedReason(screen.tail.join('\n')) + blockedReason: ready + ? null + : isAntigravityCommandApprovalScreen(screen.tail) + ? 'agent-approval-prompt' + : detectTerminalWaitBlockedReason(screen.tail.join('\n')) } } diff --git a/src/main/startup/main-process-runtime-service.ts b/src/main/startup/main-process-runtime-service.ts index 5fe914de2a2..820cfaea4db 100644 --- a/src/main/startup/main-process-runtime-service.ts +++ b/src/main/startup/main-process-runtime-service.ts @@ -85,6 +85,7 @@ export function initializeMainProcessRuntime(): OrcaRuntimeService { getSshProvider: (connectionId) => getSshPtyProvider(connectionId), onPtyStopped: clearProviderPtyState, onTerminalAgentStatus: (event) => agentHookServer.ingestTerminalStatus(event), + onTerminalScreenPermission: (event) => agentHookServer.ingestAntigravityScreenPermission(event), // Why: serve can be promoted in place, so wire the listener from startup; runtime enables desktop-only scanners only for a ready renderer. onTerminalSideEffects: (batch: TerminalSideEffectBatch) => { if (state.mainWindow && !state.mainWindow.isDestroyed()) {