fix(antigravity): validate the visible composer before resolving adopted waits

This commit is contained in:
Neil
2026-09-19 00:38:41 -07:00
parent cae60dafc2
commit 9fc2c5bea6
15 changed files with 303 additions and 5 deletions
@@ -1,5 +1,37 @@
# Antigravity readiness: what the transcripts show
## 2026-09-19: live 1.2.7 mode captures and visible-screen fallback
New recordings under `src/main/runtime/__fixtures__/`:
- `antigravity-ready-default-127.txt`: empty default-mode composer.
- `antigravity-ready-plan-127.txt`: empty composer displaying
`> Plan mode: research & plan only (shift+tab to cycle)`.
- `antigravity-ready-accept-edits-127.txt`: empty composer displaying
`> Accept-edits mode: file edits auto-approved (shift+tab to cycle)`.
- `antigravity-plan-hint-as-draft-127.txt`: the exact plan placeholder text typed
as a real, unsubmitted draft.
The ready and typed plan rows have identical text and styling. Their footers differ:
the empty composer shows `? for shortcuts`; a typed draft removes it. The captured
working screen instead shows `esc to cancel`. A bare-caret requirement alone would
reject both empty mode composers. Matching the placeholder text alone would accept
the user's draft. The installed binary reports version `1.2.1`, but its banner is
`1.2.7`; these recordings identify the banner version.
`terminal-screen-readiness.ts` now checks the complete visible composer frame and
shortcut footer, and rejects separately projected draft text. The adopted-terminal
visible-screen fallback uses it. Captured-screen tests and runtime fallback tests
cover ready, working, dialog, and draft cases, including mocked SSH snapshots.
**This is not a complete readiness fix.** The regular retained-output matcher still
has the defects below. Narrow wrapping, a scrolled-away banner, and older layouts
without the shortcut footer require further evidence and integration. The older
`antigravity-composer-multiline-unsent.txt` recording was reused from PR #20027 with
its original metadata; it was not recaptured on 1.2.7.
## Earlier investigation (1.2.0)
`findAntigravityReadyPromptIndex` in `src/main/runtime/terminal-wait-detection.ts` decides whether
an Antigravity pane is ready for a prompt. It has been written five times, each version tuned
against a five-line screen typed from memory into a `.spec.ts` fixture. Three of the first four
@@ -0,0 +1,9 @@
{
"capturedAt": "2026-09-11T08:13:59.393Z",
"platform": "darwin",
"command": ["agy"],
"cols": 120,
"rows": 40,
"note": "agy TUI 1.2.1 with 'abc' typed into the composer, then shift+enter (ESC[13;2u), leaving an empty second composer row. The tail's last content line is a bare '>' whose line above is '> abc', not the composer rule. Ground truth: not ready \u2014 a prompt sent here is appended to the operator's unsubmitted text. This is the only capture that separates the rule clause from the caret clause.",
"exitCode": 0
}
@@ -0,0 +1,25 @@
[?2026$p[?2027$p[>4m[=0;1u[?1049h[?25l[?5W[?2004h[>4;2m[=1;1u[?u
▄▀▀▄
▀▀▀▀▀▀
▀▀▀▀▀▀▀▀
▄▀▀ ▀▀▄
▄▀▀ ▀▀▄
Welcome to the Antigravity CLI. You are currently not signed in.
⣾ Signing in...␍ ⣷ [>4m[=0;1u[?1049l[>4;2m[=1;1u[?u[0 q␍
▄▀▀▄ Antigravity CLI 1.2.1
▀▀▀▀▀▀ Gemini API key
▀▀▀▀▀▀▀▀ Gemini 3.7 Flash (Low)
▄▀▀ ▀▀▄ /tmp/agycap/w1
▄▀▀ ▀▀▄
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
>
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
? for shortcutsGemini 3.7 Flash · low␍[?25h[?25lab[?25h[?25lc[?25h[?25l
 G[?25h[?25l
>
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Gemini 3.7 Flash · low␍[?25h
@@ -0,0 +1,9 @@
{
"capturedAt": "2026-09-19T07:34:50.080Z",
"platform": "darwin",
"command": ["agy", "--mode", "plan"],
"cols": 120,
"rows": 40,
"note": "Plan mode; exact placeholder wording typed as an unsent draft at 12 seconds",
"exitCode": 0
}
@@ -0,0 +1,13 @@
_Ga=q,f=32,s=1,v=1,i=31;AAAAAA==\[?2026$p[?2027$p[?5W[?2004h[>4;2m[>1u[?u[0 q␍
▄▀▀▄ Antigravity CLI 1.2.7
▀▀▀▀▀▀ Gemini API key
▀▀▀▀▀▀▀▀ Gemini 3.7 Flash (Low)
▄▀▀ ▀▀▄ ~/orca/workspaces/orca/agy-gemini-issues
▄▀▀ ▀▀▄
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
> Plan mode: research & plan only (shift+tab to cycle)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
? for shortcutsplan · Gemini 3.7 Flash · low␍[?25h[?25lPlan
 p[?25h[?25lmode: re[?25h[?25lsearch[?25h[?25l& plan[?25h[?25l only (s[?25h[?25lhift+ta[?25h[?25lb to cy[?25h[?25lcle)[?25h
@@ -0,0 +1,9 @@
{
"capturedAt": "2026-09-19T07:31:16.912Z",
"platform": "darwin",
"command": ["agy", "--mode", "accept-edits"],
"cols": 120,
"rows": 40,
"note": "Live installed CLI banner 1.2.7; accept-edits mode startup; no prompt submitted",
"exitCode": 0
}
@@ -0,0 +1,22 @@
_Ga=q,f=32,s=1,v=1,i=31;AAAAAA==\[?2026$p[?2027$p[>4m[?1049h[?25l[?5W[?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.[>4m[<1u[?1049l[>4;2m[>1u[?u[0 q␍
▄▀▀▄ Antigravity CLI 1.2.7
▀▀▀▀▀▀ Gemini API key
▀▀▀▀▀▀▀▀ Gemini 3.7 Flash (Low)
▄▀▀ ▀▀▄ ~/orca/workspaces/orca/agy-gemini-issues
▄▀▀ ▀▀▄
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
> Accept-edits mode: file edits auto-approved (shift+tab to cycle)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
? for shortcutsaccept-edits · Gemini 3.7 Flash · low␍[?25h
@@ -0,0 +1,9 @@
{
"capturedAt": "2026-09-19T07:34:15.459Z",
"platform": "darwin",
"command": ["agy"],
"cols": 120,
"rows": 40,
"note": "Live installed CLI banner 1.2.7; default mode startup; no prompt submitted",
"exitCode": 0
}
@@ -0,0 +1,11 @@
_Ga=q,f=32,s=1,v=1,i=31;AAAAAA==\[?2026$p[?2027$p[?5W[?2004h[>4;2m[>1u[?u[0 q␍
▄▀▀▄ Antigravity CLI 1.2.7
▀▀▀▀▀▀ Gemini API key
▀▀▀▀▀▀▀▀ Gemini 3.7 Flash (Low)
▄▀▀ ▀▀▄ ~/orca/workspaces/orca/agy-gemini-issues
▄▀▀ ▀▀▄
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
>
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
? for shortcutsGemini 3.7 Flash · low␍[?25h
@@ -0,0 +1,9 @@
{
"capturedAt": "2026-09-19T07:31:17.722Z",
"platform": "darwin",
"command": ["agy", "--mode", "plan"],
"cols": 120,
"rows": 40,
"note": "Live installed CLI banner 1.2.7; plan mode startup; no prompt submitted",
"exitCode": 0
}
@@ -0,0 +1,11 @@
_Ga=q,f=32,s=1,v=1,i=31;AAAAAA==\[?2026$p[?2027$p[?5W[?2004h[>4;2m[>1u[?u[0 q␍
▄▀▀▄ Antigravity CLI 1.2.7
▀▀▀▀▀▀ Gemini API key
▀▀▀▀▀▀▀▀ Gemini 3.7 Flash (Low)
▄▀▀ ▀▀▄ ~/orca/workspaces/orca/agy-gemini-issues
▄▀▀ ▀▀▄
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
> Plan mode: research & plan only (shift+tab to cycle)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
? for shortcutsplan · Gemini 3.7 Flash · low␍[?25h
@@ -0,0 +1,53 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { Terminal } from '@xterm/headless'
import { describe, expect, it } from 'vitest'
import { isKnownReadyTerminalScreen } from './terminal-screen-readiness'
async function capturedScreen(name: string): Promise<string[]> {
const terminal = new Terminal({ cols: 120, rows: 40, allowProposedApi: true })
try {
const transcript = readFileSync(join(__dirname, '__fixtures__', `${name}.txt`), 'utf8')
await new Promise<void>((resolve) => terminal.write(transcript, resolve))
const buffer = terminal.buffer.active
return Array.from(
{ length: terminal.rows },
(_, row) => buffer.getLine(buffer.baseY + row)?.translateToString(true) ?? ''
)
} finally {
terminal.dispose()
}
}
describe('Antigravity visible-screen readiness from real PTY captures', () => {
it.each([
['antigravity-ready-default-127', true],
['antigravity-ready-plan-127', true],
['antigravity-ready-accept-edits-127', true],
['antigravity-plan-hint-as-draft-127', false],
['antigravity-composer-multiline-unsent', false],
['antigravity-dialog-model-picker', false],
['antigravity-dialog-command-palette', false],
['antigravity-dialog-trust-workspace', false],
['antigravity-busy-mid-turn', false],
['antigravity-busy-turn-ended', true]
] as const)('%s is ready: %s', async (name, ready) => {
expect(isKnownReadyTerminalScreen({ tail: await capturedScreen(name) })).toBe(ready)
})
it('refuses a draft extracted separately by the terminal projection', async () => {
const tail = await capturedScreen('antigravity-ready-default-127')
expect(isKnownReadyTerminalScreen({ tail })).toBe(true)
expect(isKnownReadyTerminalScreen({ tail, draft: 'unfinished user message' })).toBe(false)
})
it('distinguishes the real mode placeholder from the same words typed by the user', async () => {
const ready = await capturedScreen('antigravity-ready-plan-127')
const draft = await capturedScreen('antigravity-plan-hint-as-draft-127')
const prompt = '> Plan mode: research & plan only (shift+tab to cycle)'
expect(ready.map((row) => row.trim())).toContain(prompt)
expect(draft.map((row) => row.trim())).toContain(prompt)
expect(isKnownReadyTerminalScreen({ tail: ready })).toBe(true)
expect(isKnownReadyTerminalScreen({ tail: draft })).toBe(false)
})
})
@@ -0,0 +1,61 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { HeadlessEmulator } from '../daemon/headless-emulator'
import { createTranscriptPane } from './agent-transcript-pane-test-harness'
import { projectTerminalVisibleLines } from './orca-runtime-terminal-projection'
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('Antigravity adopted-terminal visible readiness probe', () => {
for (const connectionId of [undefined, 'ssh-host']) {
it.each([
['antigravity-ready-default-127', true],
['antigravity-ready-plan-127', true],
['antigravity-composer-multiline-unsent', false],
['antigravity-dialog-model-picker', false],
['antigravity-busy-mid-turn', false]
] as const)(`${connectionId ?? 'local'}: %s ready=%s`, async (name, ready) => {
const emulator = new HeadlessEmulator({ cols: 120, rows: 40, scrollback: 0 })
try {
await emulator.write(readFileSync(join(__dirname, '__fixtures__', `${name}.txt`), 'utf8'))
const screen = projectTerminalVisibleLines(emulator)
const { runtime, handle } = await createTranscriptPane({
paneTitle: 'agy',
foregroundProcess: 'agy',
data: '',
connectionId
})
const read = vi.spyOn(runtime, 'readTerminal').mockResolvedValue({
handle,
status: 'running',
tail: screen.lines,
draft: screen.draft,
source: 'screen',
truncated: false,
limited: false,
oldestCursor: '0',
nextCursor: '0',
latestCursor: '0',
returnedLineCount: screen.lines.length
})
const waiting = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 600 })
await (ready
? expect(waiting).resolves.toMatchObject({ satisfied: true })
: expect(waiting).rejects.toThrow('timeout'))
expect(read).toHaveBeenCalledWith(
handle,
{},
expect.objectContaining({ visibleScreenOnly: true })
)
} finally {
emulator.dispose()
}
})
}
})
@@ -9,10 +9,8 @@ import {
VISIBLE_TERMINAL_SNAPSHOT_TIMEOUT_MS
} from './orca-runtime-postlude'
import { withTimeout } from './runtime-async-boundaries'
import {
detectTerminalWaitBlockedReason,
isKnownReadyPromptPreview
} from './terminal-wait-detection'
import { detectTerminalWaitBlockedReason } from './terminal-wait-detection'
import { isKnownReadyTerminalScreen } from './terminal-screen-readiness'
import type {
RuntimeTerminalWait,
RuntimeTerminalWaitBlockedReason
@@ -68,7 +66,7 @@ export class OrcaRuntimeWithStartTuiIdleVisibleReadProbe extends OrcaRuntimeWith
}
const snapshotText = projection.tail.join('\n')
const blockedReason = detectTerminalWaitBlockedReason(snapshotText)
if (!blockedReason && !isKnownReadyPromptPreview(snapshotText)) {
if (!blockedReason && !isKnownReadyTerminalScreen(projection)) {
return
}
const result = this.buildTuiIdleProbeResult(waiter.handle, blockedReason)
@@ -0,0 +1,27 @@
import { isKnownReadyPromptPreview } from './terminal-wait-detection'
const ANTIGRAVITY_FRAME = /^─{8,}$/
const ANTIGRAVITY_EMPTY_COMPOSERS = new Set([
'>',
'> Plan mode: research & plan only (shift+tab to cycle)',
'> Accept-edits mode: file edits auto-approved (shift+tab to cycle)'
])
export function isKnownReadyTerminalScreen(screen: { tail: string[]; draft?: string }): boolean {
if (screen.draft?.trim()) {
return false
}
const text = screen.tail.join('\n')
if (!text.toLowerCase().includes('antigravity cli')) {
return isKnownReadyPromptPreview(text)
}
const rows = screen.tail.map((row) => row.trim()).filter(Boolean)
const footer = rows.at(-1) ?? ''
// The shortcut footer disappears for typed drafts and changes to cancel while working.
return (
footer.startsWith('? for shortcuts') &&
ANTIGRAVITY_FRAME.test(rows.at(-2) ?? '') &&
ANTIGRAVITY_EMPTY_COMPOSERS.has(rows.at(-3) ?? '') &&
ANTIGRAVITY_FRAME.test(rows.at(-4) ?? '')
)
}