mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(antigravity): validate the visible composer before resolving adopted waits
This commit is contained in:
@@ -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[H[2J
|
||||
[6G[38;2;219;177;49m▄[38;2;242;146;46;48;2;246;145;46m▀[38;2;240;114;54;48;2;243;115;55m▀[38;2;240;88;59;49m▄[m
|
||||
[5G[38;2;158;195;69;48;2;134;198;78m▀[38;2;181;180;62;48;2;117;180;94m▀[38;2;226;153;61;48;2;204;149;77m▀[38;2;246;122;52;48;2;239;121;71m▀[38;2;248;106;53;48;2;225;102;82m▀[38;2;239;84;66;48;2;225;79;89m▀[m
|
||||
[4G[38;2;124;194;81;48;2;128;198;84m▀[38;2;113;194;92;48;2;84;184;129m▀[38;2;92;169;143;48;2;64;151;222m▀[38;2;92;145;179;49m▀[38;2;131;115;176m▀[38;2;116;111;195;48;2;74;126;228m▀[38;2;153;93;168;48;2;112;110;206m▀[38;2;156;91;151;48;2;143;100;180m▀[m
|
||||
[38;2;109;198;148m▄[38;2;97;195;125;48;2;98;186;213m▀[38;2;67;174;171;48;2;71;168;220m▀[m [38;2;74;128;234;48;2;61;137;251m▀[38;2;108;115;216;48;2;74;129;240m▀[38;2;101;121;225;49m▄[m
|
||||
[38;2;103;185;244m▄[38;2;107;199;163;48;2;100;182;246m▀[38;2;100;182;246;49m▀[m [38;2;56;134;251m▀[38;2;72;129;244;48;2;56;131;249m▀[38;2;61;133;252;49m▄[m
|
||||
|
||||
Welcome to the [38;2;66;133;244mAntigravity CLI[m. You are currently not signed in.
|
||||
|
||||
⣾ Signing in...␍ ⣷ [>4m[=0;1u[?1049l[>4;2m[=1;1u[?u[0 q␍[J
|
||||
[6C[38;2;219;177;49m▄[38;2;242;146;46;48;2;246;145;46m▀[38;2;240;114;54;48;2;243;115;55m▀[38;2;240;88;59;49m▄[m [38;2;122;162;247;1mAntigravity CLI 1.2.1[m
|
||||
[34D[38;2;158;195;69;48;2;134;198;78m▀[38;2;181;180;62;48;2;117;180;94m▀[38;2;226;153;61;48;2;204;149;77m▀[38;2;246;122;52;48;2;239;121;71m▀[38;2;248;106;53;48;2;225;102;82m▀[38;2;239;84;66;48;2;225;79;89m▀[m [38;2;154;165;206mGemini API key[m
|
||||
[28D[38;2;124;194;81;48;2;128;198;84m▀[38;2;113;194;92;48;2;84;184;129m▀[38;2;92;169;143;48;2;64;151;222m▀[38;2;92;145;179;49m▀[38;2;131;115;176m▀[38;2;116;111;195;48;2;74;126;228m▀[38;2;153;93;168;48;2;112;110;206m▀[38;2;156;91;151;48;2;143;100;180m▀[m [38;2;154;165;206mGemini 3.7 Flash (Low)[m
|
||||
[38;2;109;198;148m▄[38;2;97;195;125;48;2;98;186;213m▀[38;2;67;174;171;48;2;71;168;220m▀[m [38;2;74;128;234;48;2;61;137;251m▀[38;2;108;115;216;48;2;74;129;240m▀[38;2;101;121;225;49m▄[m [38;2;154;165;206m/tmp/agycap/w1[m
|
||||
[38;2;103;185;244m▄[38;2;107;199;163;48;2;100;182;246m▀[38;2;100;182;246;49m▀[m [38;2;56;134;251m▀[38;2;72;129;244;48;2;56;131;249m▀[38;2;61;133;252;49m▄[m
|
||||
|
||||
[38;2;65;72;104m────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[m
|
||||
[38;2;122;162;247m>[m
|
||||
[38;2;65;72;104m────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[m
|
||||
[38;2;154;165;206m? for shortcuts[m[83X[83C[38;2;154;165;206mGemini 3.7 Flash · low␍[m[2A[2C[?25h[?25lab[?25h[?25lc[?25h[?25l
|
||||
|
||||
[92C[1K [38;2;154;165;206mG[m[2A[94D[?25h[?25l
|
||||
>[K
|
||||
[38;2;65;72;104m────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[m
|
||||
[97C[1K [38;2;154;165;206mGemini 3.7 Flash · low␍[m[2A[3C[?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==\[c[?2026$p[?2027$p[?5W[?2004h[>4;2m[>1u[?u[0 q␍[J
|
||||
[6C▄▀▀▄ [1mAntigravity CLI 1.2.7[m
|
||||
[34D▀▀▀▀▀▀ Gemini API key
|
||||
[28D▀▀▀▀▀▀▀▀ Gemini 3.7 Flash (Low)
|
||||
▄▀▀ ▀▀▄ ~/orca/workspaces/orca/agy-gemini-issues
|
||||
▄▀▀ ▀▀▄
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
> Plan mode: research & plan only (shift+tab to cycle)
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
? for shortcuts[76X[76Cplan · Gemini 3.7 Flash · low␍[2A[2C[?25h[?25lPlan[K
|
||||
|
||||
[84C[1K p[2A[85D[?25h[?25lmode: re[?25h[?25lsearch[C[?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==\[c[?2026$p[?2027$p[>4m[?1049h[?25l[?5W[?2004h[>4;2m[>1u[?u[H[2J
|
||||
[6G▄▀▀▄
|
||||
[5G▀▀▀▀▀▀
|
||||
[4G▀▀▀▀▀▀▀▀
|
||||
▄▀▀ ▀▀▄
|
||||
▄▀▀ ▀▀▄
|
||||
|
||||
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␍[J
|
||||
[6C▄▀▀▄ [1mAntigravity CLI 1.2.7[m
|
||||
[34D▀▀▀▀▀▀ Gemini API key
|
||||
[28D▀▀▀▀▀▀▀▀ Gemini 3.7 Flash (Low)
|
||||
▄▀▀ ▀▀▄ ~/orca/workspaces/orca/agy-gemini-issues
|
||||
▄▀▀ ▀▀▄
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
> Accept-edits mode: file edits auto-approved (shift+tab to cycle)
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
? for shortcuts[68X[68Caccept-edits · Gemini 3.7 Flash · low␍[2A[2C[?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==\[c[?2026$p[?2027$p[?5W[?2004h[>4;2m[>1u[?u[0 q␍[J
|
||||
[6C▄▀▀▄ [1mAntigravity CLI 1.2.7[m
|
||||
[34D▀▀▀▀▀▀ Gemini API key
|
||||
[28D▀▀▀▀▀▀▀▀ Gemini 3.7 Flash (Low)
|
||||
▄▀▀ ▀▀▄ ~/orca/workspaces/orca/agy-gemini-issues
|
||||
▄▀▀ ▀▀▄
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
>
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
? for shortcuts[83X[83CGemini 3.7 Flash · low␍[2A[2C[?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==\[c[?2026$p[?2027$p[?5W[?2004h[>4;2m[>1u[?u[0 q␍[J
|
||||
[6C▄▀▀▄ [1mAntigravity CLI 1.2.7[m
|
||||
[34D▀▀▀▀▀▀ Gemini API key
|
||||
[28D▀▀▀▀▀▀▀▀ Gemini 3.7 Flash (Low)
|
||||
▄▀▀ ▀▀▄ ~/orca/workspaces/orca/agy-gemini-issues
|
||||
▄▀▀ ▀▀▄
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
> Plan mode: research & plan only (shift+tab to cycle)
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
? for shortcuts[76X[76Cplan · Gemini 3.7 Flash · low␍[2A[2C[?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) ?? '')
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user