diff --git a/docs/reference/antigravity-readiness-evidence.md b/docs/reference/antigravity-readiness-evidence.md index 89eddc354488..65d5b5948ad5 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/docs/site/content/docs/agents/antigravity.mdx b/docs/site/content/docs/agents/antigravity.mdx index 623b0a6d6e01..b476cc006ea0 100644 --- a/docs/site/content/docs/agents/antigravity.mdx +++ b/docs/site/content/docs/agents/antigravity.mdx @@ -7,6 +7,20 @@ Orca launches Google's **Antigravity CLI**, whose command is `agy`. Installing t Antigravity desktop application alone does not establish that this command is available. The separate `gemini` command is a different agent entry in Orca. +## Existing Gemini CLI sessions + +Orca labels the Gemini CLI integration as **legacy** and recommends Antigravity for +new Google CLI sessions. If both CLIs are detected and no preferred agent is selected, +Antigravity comes before Gemini in automatic selection. An explicit Gemini preference +still launches Gemini; disabling or not installing Antigravity does not remove the +Gemini fallback. + +Existing Gemini history, session resume, and account settings remain available. +Gemini account settings do not authenticate Antigravity, and a Gemini session ID +cannot be reused as an Antigravity conversation ID. Authenticate `agy` separately +on its execution host. Gemini model names returned by `agy models` are still valid +Antigravity model names; the legacy label refers to the CLI provider. + ## Check the CLI first Install the CLI using [Google's current instructions](https://antigravity.google/docs/cli-overview). 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 000000000000..a00e71142f6d --- /dev/null +++ b/src/main/agent-hooks/server-ingest-antigravity-screen.test.ts @@ -0,0 +1,155 @@ +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, + clearedState: 'done' + }) + ).toBe(true) + expect(baseline(server)).toMatchObject({ state: 'done', prompt: 'print marker' }) + expect(baseline(server).interactivePrompt).toBeUndefined() + expect(baseline(server).toolName).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, + clearedState: 'done' + }) + ).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) + }) +}) + +it('accepts the runtime-attested binding absent from a native hook row', () => { + const server = new AgentHookServer() + server.ingestTerminalStatus({ + paneKey: PANE, + payload: { state: 'working', agentType: 'antigravity', prompt: 'print marker' } + }) + const row = baseline(server) + expect(row.terminalHandle).toBeUndefined() + expect(server.ingestAntigravityScreenPermission({ baseline: row, command: 'echo OK' })).toBe( + false + ) + expect( + server.ingestAntigravityScreenPermission({ + baseline: { ...row, terminalHandle: 'term-runtime' }, + command: 'echo OK' + }) + ).toBe(true) + expect(baseline(server)).toMatchObject({ state: 'waiting', terminalHandle: 'term-runtime' }) +}) 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 000000000000..edb7d53527f0 --- /dev/null +++ b/src/main/agent-hooks/server/server-ingest-antigravity-screen.ts @@ -0,0 +1,79 @@ +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 } | { command: null; clearedState: 'working' | 'done' }) + +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 !== undefined && + 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 + } + // Only a positively identified working or idle screen can clear our approval. + return ( + this.applyNormalizedStatus( + { + paneKey: previous.paneKey, + tabId: previous.tabId, + worktreeId: previous.worktreeId, + connectionId: previous.connectionId, + terminalHandle: baseline.terminalHandle, + providerSession: previous.providerSession, + payload: { + ...previous.payload, + state: request.command === null ? request.clearedState : 'waiting', + ...(command === null ? { toolName: undefined, toolInput: undefined } : {}), + 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 e7e681156591..a927e8fc0372 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 fb22f92c392e..6d9f2c6b28a4 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-approved-command-running.meta.json b/src/main/runtime/__fixtures__/antigravity-windows-approved-command-running.meta.json new file mode 100644 index 000000000000..e12cc9bf8ddc --- /dev/null +++ b/src/main/runtime/__fixtures__/antigravity-windows-approved-command-running.meta.json @@ -0,0 +1,9 @@ +{ + "capturedAt": "2026-09-20T10:03:18.668Z", + "platform": "win32", + "command": ["C:\\Users\\user\\AppData\\Local\\agy\\bin\\agy.exe"], + "cols": 120, + "rows": 40, + "note": "Native Windows agy 1.2.7; approved 20-second command still running; key 1 at 45 seconds; 120x40.", + "exitCode": 1 +} diff --git a/src/main/runtime/__fixtures__/antigravity-windows-approved-command-running.txt b/src/main/runtime/__fixtures__/antigravity-windows-approved-command-running.txt new file mode 100644 index 000000000000..bdd12fd1c07f --- /dev/null +++ b/src/main/runtime/__fixtures__/antigravity-windows-approved-command-running.txt @@ -0,0 +1,194 @@ +[?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 toolto run Start-Sleep-Seconds 20; Write-OutputORCA_BUSY_CAPTURE_OK.Donotmodifyfiles.[?25l[?25h[?25l? for shortcuts[?25h[?25l +> Use the terminal tool to run Start-Sleep -Seconds 20; Write-Output ORCA_BUSY_CAPTURE_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⣻ Generatin[?25h[?25l⣽ Generating.[?25h[?25l⣾ Generating..[?25h[?25l⣷ Generating...[?25h[?25l⣯ Generatin[?25h[?25l⣟ Generating[?25h[?25l..[?25h[?25l⡿ Generating..[?25h[?25l⢿ Generating...[?25h[?25l⣻ [?25h[?25l⣽ [?25h[?25l⣾ [?25h[?25l⣷ [?25h[?25l⣯ [?25h[?25l⣟ G[?25h[?25l⡿ Gen[?25h[?25l⢿ Gene[?25h[?25l⣻ Gener[?25h[?25l⣽ Genera[?25h[?25l⣾ Generati[?25h[?25l⣷ Generatin[?25h[?25l⣯ Generating[?25h[?25l⣟ Generating.[?25h[?25l I'm now setting up the execution. I've formulated the exact command I need to use, which involves a sleep function... +└ Tip: You can switch conversations with /resume. +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low[?25h[?25l⡿[?25h[?25letting[?25h[?25lsettin[?25h[?25l⢿ I'm now setti[?25h[?25lw sett[?25h[?25low set[?25h[?25l⣻[?25h[?25lnow se[?25h[?25l now s[?25h[?25l⣽[?25h[?25lm now [?25h[?25l'm now[?25h[?25l⣾ I'm no[?25h[?25ln[?25h[?25l [?25h[?25l⣷[?25h[?25lm[?25h[?25l'[?25h[?25l I[?25h[?25l⣯[?25h[?25l +▸ Thought for 3s, 186 tokens + I'm now setting up the execution. I've formulated the exact command I need to use, which involves a sleep function...  + +○ Bash(Start-Sleep -Seconds 20; Write-Output ORCA_BUSY_CAPTURE_OK) (ctrl+o to expand) +⣯ Running command... +└ Tip: You can switch conversations with /resume. +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low[?25h[0 q[?25l●  + +Command +Requesting permission for: + Start-Sleep -Seconds 20; Write-Output ORCA_BUSY_CAPTURE_OK + +Run this command? +> 1. Yes, run command + 2. Yes, and always allow in this conversation for commands that start with 'Start-Sleep -Seconds 20; Write-Output +ORCA_BUSY_CAPTURE_OK' + 3. Yes, and always allow for commands that start with 'Start-Sleep -Seconds 20; Write-Output ORCA_BUSY_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[0 q⣟ Running command... +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low + + + + + + + + + + + + + + + + + + + + +[?25h[?25l⡿ [?25h[?25l⢿ Running[?25h[?25l⣻ Running c[?25h[?25l⣽ Running co[?25h[?25l⣾ Running com[?25h[?25lg comm[?25h[?25l⣷  ● [03:02:48] Start-Sleep -Seconds 20; Write-Output ORCA_BUSY_CAPTURE_OK running +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low · 1 task(s) · /tasks[?25h[?25l⣯ Running comman[?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⣷ Running command..[?25h[?25l⣯ Running command...[?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 [?25h[?25l⣻ Running co[?25h[?25l⣽ Running com[?25h[?25l⣾ Running comm[?25h[?25l⣷ Running comma[?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⣾ Running command..[?25h[?25l⣷ Running command...[?25h[?25l⣯ [?25h[?25l⣟ [?25h[?25l⡿ [?25h[?25l + I've started the backgro +⡿ Running command...> ● [03:02:48] Start-Sleep -Seconds 20; Write-Output ORCA_BUSY_CAPTURE_OK running +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low · 1 task(s) · /tasks[?25h[?25lund task to +⢿ [?25h[?25lrun the comm[?25h[?25land Start-S[?25h[?25lleep -Second[?25h[?25ls [?25h[?25l Ru[?25h[?25l⣻ [?25h[?25l20; Write-Ou[?25h[?25ltput ORCA_BUSY_CAPTURE_OK. The + system will notify us when it completes. +⣻ Running command... +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ● [03:02:48] Start-Sleep -Seconds 20; Write-Output ORCA_BUSY_CAPTURE_OK running +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +esc to cancel Gemini 3.1 Pro · low · 1 task(s) · /tasks[?25h[?25l⣽ Ru[?25h[?25l? for shortcuts[?25h \ No newline at end of file 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 000000000000..d4a019e16738 --- /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 000000000000..48996981f235 --- /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 000000000000..9cf6c09e06e3 --- /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 000000000000..7b701d36ccc8 --- /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 000000000000..ce03cc4c86fb --- /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 000000000000..55aa2d5a343f --- /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 1f399e0464f0..bd45c4bf3ef3 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 000000000000..f51ed024e7b1 --- /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 000000000000..d3db4436c679 --- /dev/null +++ b/src/main/runtime/antigravity-command-approval-transcript.test.ts @@ -0,0 +1,92 @@ +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.each([ + ['antigravity-windows-command-cancelled', 'done'], + ['antigravity-busy-mid-turn', 'working'], + ['antigravity-windows-approved-command-running', 'working'] +] as const)('publishes permission then the captured %s state', async (capture, state) => { + 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, + 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__/${capture}.txt`), 'utf8') + runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, cancelled, Date.now()) + await vi.waitFor(() => expect(wiring.statusStore.getStatusSnapshot()[0]).toMatchObject({ state })) + 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 000000000000..dda969bbde21 --- /dev/null +++ b/src/main/runtime/antigravity-screen-permission-publisher.test.ts @@ -0,0 +1,132 @@ +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.each([ + null, + { + lines: ['tool output without a recognized composer'], + generation: 1, + sequence: 1, + isAlternateScreen: false + } + ])('does not clear permission from a missing or unrecognized screen: %s', async (screen) => { + const readScreen = vi.fn(async () => screen) + const publish = vi.fn(() => true) + const publisher = new AntigravityScreenPermissionPublisher({ + baseline: () => ({ ...baseline, state: 'waiting' }), + readScreen, + isCurrent: () => true, + publish + }) + publisher.schedule('pty') + await vi.waitFor(() => expect(readScreen).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 000000000000..3956287067e0 --- /dev/null +++ b/src/main/runtime/antigravity-screen-permission-publisher.ts @@ -0,0 +1,73 @@ +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 { + isAntigravityWorkingTerminalScreen, + 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 (isAntigravityWorkingTerminalScreen(screen.lines)) { + this.deps.publish({ baseline, command: null, clearedState: 'working' }) + } else if (isKnownReadyTerminalScreen({ tail: screen.lines, draft: screen.draft })) { + this.deps.publish({ baseline, command: null, clearedState: 'done' }) + } + } 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 160c99f20717..c780df49da2e 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 33b4ace77720..f489e69cc8a8 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 a28ead157248..04ed74ed5095 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 3c3342855094..44385ba13a57 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,31 @@ 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 + } + const terminalHandle = this.getAgentStatusTerminalHandleForPaneKey(pty.paneKey) + const row = this.getAgentProviderSessionRowsForPaneFn?.(pty.paneKey)?.find( + (candidate) => + candidate.providerSessionOnly !== true && candidate.agentType === 'antigravity' + ) + // Native hooks identify the pane; the runtime owns its live terminal binding. + return row && terminalHandle ? { ...row, terminalHandle } : 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 30fc8fe56f2f..b53a3b4a4067 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,28 @@ 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')) } } + +/** Captured foreground generation and Windows background-task layouts. */ +export function isAntigravityWorkingTerminalScreen(tail: readonly string[]): boolean { + const rows = tail.map((row) => row.trim()).filter(Boolean) + const footer = rows.at(-1) ?? '' + const composerAt = (offset: number): boolean => + ANTIGRAVITY_FRAME.test(rows.at(offset) ?? '') && + ANTIGRAVITY_EMPTY_COMPOSERS.has(rows.at(offset - 1) ?? '') && + ANTIGRAVITY_FRAME.test(rows.at(offset - 2) ?? '') + return ( + (footer.startsWith('esc to cancel') && composerAt(-2)) || + (footer.startsWith('? for shortcuts') && + /· [1-9]\d* task\(s\) · \/tasks$/.test(footer) && + ANTIGRAVITY_FRAME.test(rows.at(-2) ?? '') && + /^● \[\d{2}:\d{2}:\d{2}\] .+ running$/.test(rows.at(-3) ?? '') && + composerAt(-4)) + ) +} diff --git a/src/main/startup/main-process-runtime-service.ts b/src/main/startup/main-process-runtime-service.ts index 5fe914de2a2a..820cfaea4dbb 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()) { diff --git a/src/renderer/src/components/settings/accounts-pane-provider-setting-sections.tsx b/src/renderer/src/components/settings/accounts-pane-provider-setting-sections.tsx index 6bf03df51f42..53c7cf1bdac2 100644 --- a/src/renderer/src/components/settings/accounts-pane-provider-setting-sections.tsx +++ b/src/renderer/src/components/settings/accounts-pane-provider-setting-sections.tsx @@ -15,20 +15,20 @@ export function renderGeminiAccountsSection(model: AccountsPaneSectionModel): Re

- {translate('auto.components.settings.AccountsPane.0c64dc2a64', 'Gemini')} + {translate('auto.components.settings.AccountsPane.geminiLegacy', 'Gemini CLI (legacy)')}

{translate( - 'auto.components.settings.AccountsPane.973741a871', - 'Configure Gemini provider settings.' + 'auto.components.settings.AccountsPane.geminiLegacyDescription', + 'For new Google CLI sessions, use Antigravity (agy). These settings remain available for existing Gemini CLI accounts and do not sign you into Antigravity.' )}

[ export const getAccountsGeminiSearchEntries = createLocalizedCatalog(() => [ { title: translate( - 'auto.components.settings.accounts.search.d819755b02', - 'Use Gemini CLI credentials' + 'auto.components.settings.accounts.search.geminiLegacy', + 'Use Gemini CLI credentials (legacy)' ), description: translate( 'auto.components.settings.accounts.search.bada4a3218', diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 1953760f040e..02be8a0edc4a 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -1078,6 +1078,8 @@ }, "settings": { "AccountsPane": { + "0c64dc2a64": "Gemini", + "0c7f915b01": "Use Gemini CLI credentials", "15e831350e": "Configure MiniMax usage tracking from platform.minimax.io.", "1fd1b1b6b4": "Cookie not set", "3455cf43fa": "Claude login.", @@ -1087,6 +1089,7 @@ "5e08b0fe57": "Stored locally and sent only to platform.minimax.io for usage refreshes.", "79418c782a": "Open platform.minimax.io/console/usage in your browser, sign in, then copy the Cookie request header from DevTools (Network → any remains request → Cookie).", "9107406589": "Could not load Claude accounts.", + "973741a871": "Configure Gemini provider settings.", "b10cb4f696": "adding", "b11078a9c2": "wsl", "b43e761fe5": "MiniMax cookie update failed.", @@ -1561,6 +1564,11 @@ "7c3bb36706": "remove", "e2b0ee267f": "stale" }, + "accounts": { + "search": { + "d819755b02": "Use Gemini CLI credentials" + } + }, "agent-awake-copy": { "95d3031db2": "Keeps this computer and display awake while agents are working. Lid-close behavior follows this device's power settings.", "a42f6fbdd8": "Keeps this computer and display awake while agents are working. Orca also asks this device to stay awake when the lid is closed, subject to its power policy.", @@ -2585,6 +2593,11 @@ } }, "lib": { + "agent": { + "catalog": { + "12e6baa4f7": "Gemini" + } + }, "browser": { "cookie": { "import": { diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 244b22313ba0..f7dc92e421fe 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -607,7 +607,8 @@ "da41abbdd4": "Ante", "060d152fb5": "Trae", "d443a47995": "Prime Agent", - "mimo_code_label": "MiMo Code" + "mimo_code_label": "MiMo Code", + "geminiLegacy": "Gemini CLI (legacy)" }, "skill": { "cli": { @@ -6582,7 +6583,10 @@ "4e32e030b2": "Stored locally. Orca sends it only to platform.minimax.io for usage refreshes.", "5e08b0fe57": "Stored locally and sent only to platform.minimax.io for usage refreshes.", "79418c782a": "Open platform.minimax.io/console/usage in your browser, sign in, then copy the Cookie request header from DevTools (Network → any remains request → Cookie).", - "f5d8d2a6a1": "Open platform.minimax.io/console/usage in your browser and sign in." + "f5d8d2a6a1": "Open platform.minimax.io/console/usage in your browser and sign in.", + "geminiLegacy": "Gemini CLI (legacy)", + "geminiLegacyDescription": "For new Google CLI sessions, use Antigravity (agy). These settings remain available for existing Gemini CLI accounts and do not sign you into Antigravity.", + "geminiLegacyCredentials": "Use Gemini CLI credentials (legacy)" }, "AdvancedPane": { "40b29e0bf3": "Restart", @@ -8968,7 +8972,8 @@ "b2c4e7f1a8": "endpoint", "3a9b6d2c4e": "api key", "5d8f1a3b7c": "china", - "7e2a4b8c1d": "overseas" + "7e2a4b8c1d": "overseas", + "geminiLegacy": "Use Gemini CLI credentials (legacy)" } }, "advanced": { diff --git a/src/renderer/src/lib/agent-catalog.tsx b/src/renderer/src/lib/agent-catalog.tsx index cf46fd9aae75..30c9e6453c5c 100644 --- a/src/renderer/src/lib/agent-catalog.tsx +++ b/src/renderer/src/lib/agent-catalog.tsx @@ -136,13 +136,6 @@ export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] => faviconDomain: 'primeintellect.ai', homepageUrl: 'https://github.com/PrimeIntellect-ai/prime-agent' }, - { - id: 'gemini', - label: translate('auto.lib.agent.catalog.12e6baa4f7', 'Gemini'), - cmd: 'gemini', - faviconDomain: 'gemini.google.com', - homepageUrl: 'https://github.com/google-gemini/gemini-cli' - }, { id: 'antigravity', label: translate('auto.lib.agent.catalog.691dd11789', 'Antigravity'), @@ -150,6 +143,13 @@ export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] => faviconDomain: 'antigravity.google', homepageUrl: 'https://antigravity.google/docs/cli-overview' }, + { + id: 'gemini', + label: translate('auto.lib.agent.catalog.geminiLegacy', 'Gemini CLI (legacy)'), + cmd: 'gemini', + faviconDomain: 'gemini.google.com', + homepageUrl: 'https://github.com/google-gemini/gemini-cli' + }, { id: 'aider', label: translate('auto.lib.agent.catalog.b32627f09b', 'Aider'), diff --git a/src/renderer/src/lib/agent-paste-draft.ts b/src/renderer/src/lib/agent-paste-draft.ts index 7522d91298b1..5a3b6b74f841 100644 --- a/src/renderer/src/lib/agent-paste-draft.ts +++ b/src/renderer/src/lib/agent-paste-draft.ts @@ -36,11 +36,7 @@ export const BRACKETED_PASTE_BEGIN = BRACKETED_PASTE_START export { BRACKETED_PASTE_END } export const POST_PASTE_SUBMIT_DELAY_MS = 50 -// Why: "the tab has a PTY" and "the agent's composer accepts input" are separate -// states with separate failure modes, so they get separate budgets. A PTY that -// hasn't appeared in 8s means the launch itself failed — waiting the (longer) -// composer budget on top would only delay that verdict. Keeping them distinct -// also stops one slow step from spending the other's budget (STA-3367). +// PTY binding and composer readiness have separate budgets (STA-3367). const PTY_SPAWN_TIMEOUT_MS = 8000 export function getSettingsForAgentTabRuntimeOwner( @@ -101,7 +97,11 @@ export async function pasteDraftWhenAgentReady(args: { const readinessTimeoutMs = resolveDraftPasteReadyTimeoutMs(agent, timeoutMs) const readiness = await waitForAgentDraftInputReadyOnTab({ tabId, - spawnTimeoutMs: PTY_SPAWN_TIMEOUT_MS, + // Windows Antigravity launches can bind their PTY after the ordinary eight-second budget. + spawnTimeoutMs: + agent === 'antigravity' + ? Math.max(PTY_SPAWN_TIMEOUT_MS, readinessTimeoutMs) + : PTY_SPAWN_TIMEOUT_MS, readinessTimeoutMs, agent, readySignal, diff --git a/src/renderer/src/lib/antigravity-continuation-delivery.test.ts b/src/renderer/src/lib/antigravity-continuation-delivery.test.ts index db7400798fe7..530c1778fb2c 100644 --- a/src/renderer/src/lib/antigravity-continuation-delivery.test.ts +++ b/src/renderer/src/lib/antigravity-continuation-delivery.test.ts @@ -1,20 +1,36 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { pasteDraftToAgentPtyWhenReady, pasteDraftWhenAgentReady } from './agent-paste-draft' -const mocks = vi.hoisted(() => ({ - hostReady: vi.fn(), - shellReady: vi.fn(), - inspect: vi.fn(), - send: vi.fn(), - processReady: vi.fn() -})) +const mocks = vi.hoisted(() => { + const state: { + settings: object + ptyIdsByTabId: Record + tabsByWorktree: object + } = { + settings: {}, + ptyIdsByTabId: { tab: ['pty'] }, + tabsByWorktree: {} + } + return { + state, + subscribers: new Set<(snapshot: typeof state) => void>(), + hostReady: vi.fn(), + shellReady: vi.fn(), + inspect: vi.fn(), + send: vi.fn(), + processReady: vi.fn() + } +}) vi.mock('./antigravity-draft-readiness', () => ({ waitForAntigravityDraftReady: mocks.hostReady })) vi.mock('./agent-draft-readiness', () => ({ waitForAgentDraftInputReady: mocks.shellReady })) vi.mock('./agent-ready-wait', () => ({ waitForAgentReady: mocks.processReady })) vi.mock('@/store', () => ({ useAppStore: { - getState: () => ({ settings: {}, ptyIdsByTabId: { tab: ['pty'] }, tabsByWorktree: {} }), - subscribe: () => () => {} + getState: () => mocks.state, + subscribe: (listener: (state: typeof mocks.state) => void) => { + mocks.subscribers.add(listener) + return () => mocks.subscribers.delete(listener) + } } })) vi.mock('@/runtime/runtime-terminal-inspection', () => ({ @@ -26,6 +42,8 @@ describe('Antigravity continuation delivery', () => { beforeEach(() => { vi.useFakeTimers() vi.stubGlobal('window', { setTimeout, clearTimeout }) + mocks.state.ptyIdsByTabId = { tab: ['pty'] } + mocks.subscribers.clear() mocks.hostReady.mockReset() mocks.shellReady .mockReset() @@ -43,6 +61,56 @@ describe('Antigravity continuation delivery', () => { vi.unstubAllGlobals() }) + it('retains the handoff when Windows binds the PTY after eight seconds', async () => { + mocks.state.ptyIdsByTabId = { tab: [] } + mocks.hostReady.mockResolvedValue(true) + const onTimeout = vi.fn() + const waiting = pasteDraftWhenAgentReady({ + tabId: 'tab', + agent: 'antigravity', + content: 'late Windows handoff', + submit: true, + forcePaste: true, + onTimeout + }) + await vi.advanceTimersByTimeAsync(8307) + expect(mocks.send).not.toHaveBeenCalled() + mocks.state.ptyIdsByTabId.tab = ['pty'] + for (const listener of mocks.subscribers) { + listener(mocks.state) + } + await vi.advanceTimersByTimeAsync(100) + await expect(waiting).resolves.toBe(true) + expect(onTimeout).not.toHaveBeenCalled() + expect(mocks.hostReady).toHaveBeenCalledOnce() + expect(mocks.send).toHaveBeenLastCalledWith({}, 'pty', '\r') + expect(mocks.subscribers.size).toBe(0) + }) + + it.each([ + { agent: 'antigravity', timeoutMs: 60000 }, + { agent: 'claude', timeoutMs: 8000 } + ] as const)( + 'bounds missing-PTY waits for $agent at $timeoutMs ms', + async ({ agent, timeoutMs }) => { + mocks.state.ptyIdsByTabId = { tab: [] } + const onTimeout = vi.fn() + const waiting = pasteDraftWhenAgentReady({ + tabId: 'tab', + agent, + content: 'unsent handoff', + forcePaste: true, + onTimeout + }) + await vi.advanceTimersByTimeAsync(timeoutMs) + await expect(waiting).resolves.toBe(false) + expect(onTimeout).toHaveBeenCalledOnce() + expect(mocks.hostReady).not.toHaveBeenCalled() + expect(mocks.send).not.toHaveBeenCalled() + expect(mocks.subscribers.size).toBe(0) + } + ) + it('leaves context unwritten through a quiet shell until the host confirms the composer', async () => { mocks.hostReady.mockImplementation( () => new Promise((resolve) => setTimeout(() => resolve(true), 12000)) diff --git a/src/renderer/src/lib/orchestration-skill-coverage.test.ts b/src/renderer/src/lib/orchestration-skill-coverage.test.ts index d0128396e4e7..4af6115ddbde 100644 --- a/src/renderer/src/lib/orchestration-skill-coverage.test.ts +++ b/src/renderer/src/lib/orchestration-skill-coverage.test.ts @@ -58,7 +58,7 @@ describe('orchestration skill agent coverage', () => { ) ).toEqual([ { agent: 'codex', label: 'Codex', installed: true }, - { agent: 'gemini', label: 'Gemini', installed: true }, + { agent: 'gemini', label: 'Gemini CLI (legacy)', installed: true }, { agent: 'droid', label: 'Droid', installed: true } ]) }) diff --git a/src/shared/tui-agent-selection.test.ts b/src/shared/tui-agent-selection.test.ts index c92f5e62d376..b093e78bdb90 100644 --- a/src/shared/tui-agent-selection.test.ts +++ b/src/shared/tui-agent-selection.test.ts @@ -6,6 +6,12 @@ import { } from './tui-agent-selection' describe('pickTuiAgent', () => { + it('prefers Antigravity for new Google CLI sessions but preserves explicit Gemini choices', () => { + expect(pickTuiAgent(null, ['gemini', 'antigravity'])).toBe('antigravity') + expect(pickTuiAgent('gemini', ['gemini', 'antigravity'])).toBe('gemini') + expect(pickTuiAgent(null, ['gemini', 'antigravity'], ['antigravity'])).toBe('gemini') + expect(pickTuiAgent(null, ['gemini'])).toBe('gemini') + }) it('uses an installed preferred agent', () => { expect(pickTuiAgent('codex', ['claude', 'codex'])).toBe('codex') }) diff --git a/src/shared/tui-agent-selection.ts b/src/shared/tui-agent-selection.ts index 86216e589b82..d3cc279ac8bc 100644 --- a/src/shared/tui-agent-selection.ts +++ b/src/shared/tui-agent-selection.ts @@ -17,8 +17,8 @@ export const TUI_AGENT_AUTO_PICK_ORDER = [ 'pi', 'omp', 'prime-agent', - 'gemini', 'antigravity', + 'gemini', 'aider', 'goose', 'amp',