diff --git a/docs/reference/antigravity-readiness-evidence.md b/docs/reference/antigravity-readiness-evidence.md index a93421392a0..3c65c631863 100644 --- a/docs/reference/antigravity-readiness-evidence.md +++ b/docs/reference/antigravity-readiness-evidence.md @@ -1,5 +1,27 @@ # Antigravity readiness: what the transcripts show +## 2026-09-19: ordinary wait and delivery integration + +The ordinary wait paths now consult the same current-screen classifier for +Antigravity, including immediate checks, polling, title callbacks and the queued +message delivery gate. Snapshot reads reuse the execution host's existing terminal +model/provider path and pending-read deduplication. Cached screens are rejected +after output, process generation changes or queued headless reflow/writes. + +A hidden rebuilt Orca with installed agy reproduced the previous failure: an empty +composer reported the dismissed trust prompt, then timed out after an app restart. +With this integration, the same daemon terminal returns ready. Typing the exact +Plan placeholder makes the wait time out; clearing it returns ready again. Screenshots +and wait results were inspected together. No model generation was needed for this +check; successful authenticated worker turns remain unverified. + +Runtime tests replay recorded output through the normal wait path, exercise a +trust dialog before its recorded alternate-screen teardown, then replay the ready +screen. They also cover drafts, working output, cache invalidation on new output +and reflow, and queued delivery retry after a draft becomes empty. Narrow/wrapped +layouts and absent banners remain unrecognized rather than claimed ready. Real +SSH/Windows execution and disconnect freshness still require validation. + ## 2026-09-19: live 1.2.7 mode captures and visible-screen fallback New recordings under `src/main/runtime/__fixtures__/`: @@ -24,8 +46,8 @@ shortcut footer, and rejects separately projected draft text. The adopted-termin 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 +**This was initially a fallback-only fix.** The ordinary integration above now +bypasses the defective retained-output matcher for recognized Antigravity panes. 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. diff --git a/src/main/runtime/antigravity-ordinary-wait.test.ts b/src/main/runtime/antigravity-ordinary-wait.test.ts new file mode 100644 index 00000000000..d945e887799 --- /dev/null +++ b/src/main/runtime/antigravity-ordinary-wait.test.ts @@ -0,0 +1,130 @@ +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' + +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') } +})) + +function capture(name: string): string { + const raw = readFileSync(join(__dirname, '__fixtures__', `${name}.txt`), 'utf8') + const teardown = + name === 'antigravity-dialog-trust-workspace' ? raw.lastIndexOf('\x1b[?1049l') : -1 + // Replay the recorded live dialog before capture shutdown leaves its alternate screen. + return teardown === -1 ? raw : raw.slice(0, teardown) +} + +describe('Antigravity ordinary waits use the current screen', () => { + it.concurrent.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-busy-mid-turn', false] + ] as const)('%s ready=%s', async (name, ready) => { + const { runtime, handle } = await createTranscriptPane({ + paneTitle: 'agy', + foregroundProcess: 'agy', + data: '' + }) + runtime.seedHeadlessTerminal(TRANSCRIPT_PANE_PTY_ID, '\x1b[0m', { cols: 120, rows: 40 }) + runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, capture(name), Date.now()) + const waiting = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 3500 }) + await (ready + ? expect(waiting).resolves.toMatchObject({ satisfied: true }) + : expect(waiting).rejects.toThrow('timeout')) + }) + + it('forgets a trust dialog after the real ready-screen redraw', async () => { + const { runtime, handle } = await createTranscriptPane({ + paneTitle: 'agy', + foregroundProcess: 'agy', + data: '' + }) + runtime.seedHeadlessTerminal(TRANSCRIPT_PANE_PTY_ID, '\x1b[0m', { cols: 120, rows: 40 }) + runtime.onPtyData( + TRANSCRIPT_PANE_PTY_ID, + capture('antigravity-dialog-trust-workspace'), + Date.now() + ) + await expect( + runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 3500 }) + ).resolves.toMatchObject({ satisfied: false, blockedReason: 'agent-trust-workspace' }) + runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, capture('antigravity-ready-default-127'), Date.now()) + await expect( + runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 3500 }) + ).resolves.toMatchObject({ satisfied: true }) + }, 10000) + + it('invalidates a ready screen before draft output finishes parsing', async () => { + const { runtime, handle } = await createTranscriptPane({ + paneTitle: 'agy', + foregroundProcess: 'agy', + data: '' + }) + runtime.seedHeadlessTerminal(TRANSCRIPT_PANE_PTY_ID, '\x1b[0m', { cols: 120, rows: 40 }) + runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, capture('antigravity-ready-plan-127'), Date.now()) + await expect( + runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 3500 }) + ).resolves.toMatchObject({ satisfied: true }) + runtime.onPtyData( + TRANSCRIPT_PANE_PTY_ID, + capture('antigravity-plan-hint-as-draft-127'), + Date.now() + ) + await expect( + runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 2500 }) + ).rejects.toThrow('timeout') + }, 10000) + + it('invalidates a ready screen when the terminal grid reflows', async () => { + const { runtime, handle } = await createTranscriptPane({ + paneTitle: 'agy', + foregroundProcess: 'agy', + data: '' + }) + runtime.seedHeadlessTerminal(TRANSCRIPT_PANE_PTY_ID, '\x1b[0m', { cols: 120, rows: 40 }) + runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, capture('antigravity-ready-plan-127'), Date.now()) + await expect( + runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 3500 }) + ).resolves.toMatchObject({ satisfied: true }) + runtime.reflowHeadlessTerminalToPtyGrid(TRANSCRIPT_PANE_PTY_ID, 20, 40) + await expect( + runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 2500 }) + ).rejects.toThrow('timeout') + }, 10000) + + it('rechecks queued delivery after a draft becomes an empty composer', async () => { + const { runtime, handle } = await createTranscriptPane({ + paneTitle: 'agy', + foregroundProcess: 'agy', + data: '' + }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these protected methods are exercised by the real delivery path; only the final delivery action is spied on. + const delivery = runtime as unknown as { + checkDeliverySettledAndArmRecheck(leaf: { tabId: string; leafId: string }): boolean + deliverPendingMessagesForLeaf(leaf: unknown): void + } + const deliver = vi.spyOn(delivery, 'deliverPendingMessagesForLeaf').mockImplementation(() => {}) + const leaf = { tabId: 'tab-1', leafId: '11111111-1111-4111-8111-111111111111' } + runtime.seedHeadlessTerminal(TRANSCRIPT_PANE_PTY_ID, '\x1b[0m', { cols: 120, rows: 40 }) + runtime.onPtyData( + TRANSCRIPT_PANE_PTY_ID, + capture('antigravity-plan-hint-as-draft-127'), + Date.now() + ) + expect(delivery.checkDeliverySettledAndArmRecheck(leaf)).toBe(false) + await expect( + runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 3500 }) + ).rejects.toThrow('timeout') + expect(deliver).not.toHaveBeenCalled() + runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, capture('antigravity-ready-plan-127'), Date.now()) + await vi.waitFor(() => expect(deliver).toHaveBeenCalled(), { timeout: 4500 }) + }, 10000) +}) diff --git a/src/main/runtime/orca-runtime-resolve-exit-waiters.ts b/src/main/runtime/orca-runtime-resolve-exit-waiters.ts index ea6329db3e6..1394a2e2911 100644 --- a/src/main/runtime/orca-runtime-resolve-exit-waiters.ts +++ b/src/main/runtime/orca-runtime-resolve-exit-waiters.ts @@ -103,6 +103,13 @@ export class OrcaRuntimeWithResolveExitWaiters extends OrcaRuntimeWithBindPtyInc // Why: the primary OSC-title signal can't fire for daemon-hosted terminals (no PTY data through the runtime), so this fallback polls the renderer-synced tab title + foreground-process quiescence; self-cancels when the OSC path fires. protected isTuiIdleSatisfiedForLeaf(leaf: RuntimeLeafRecord): boolean { + const screen = this.getTerminalScreenReadiness( + leaf.ptyId, + buildTerminalWaitText(leaf.tailBuffer, leaf.tailPartialLine, leaf.preview) + ) + if (screen) { + return screen.ready + } return isTuiIdleSatisfied({ record: leaf, rendererTitle: leaf.paneTitle ?? this.tabs.get(leaf.tabId)?.title ?? null, @@ -188,6 +195,13 @@ export class OrcaRuntimeWithResolveExitWaiters extends OrcaRuntimeWithBindPtyInc } protected isTuiIdleSatisfiedForPty(pty: RuntimePtyWorktreeRecord): boolean { + const screen = this.getTerminalScreenReadiness( + pty.ptyId, + buildTerminalWaitText(pty.tailBuffer, pty.tailPartialLine, pty.preview) + ) + if (screen) { + return screen.ready + } return isTuiIdleSatisfied({ record: pty, readPositiveBodyEvidence: () => diff --git a/src/main/runtime/orca-runtime-runtime-id.ts b/src/main/runtime/orca-runtime-runtime-id.ts index 77fd49e8899..63969c52272 100644 --- a/src/main/runtime/orca-runtime-runtime-id.ts +++ b/src/main/runtime/orca-runtime-runtime-id.ts @@ -330,6 +330,7 @@ export class OrcaRuntimeWithRuntimeId { ) protected readonly terminalIdlePolls = new RuntimeTerminalIdlePolls({ + getScreenReadiness: (ptyId, text) => this.getTerminalScreenReadiness(ptyId, text), intervalMs: TUI_IDLE_POLL_INTERVAL_MS, quiescenceMs: TUI_IDLE_QUIESCENCE_MS, getTabTitle: (tabId) => this.tabs.get(tabId)?.title ?? null, @@ -344,6 +345,7 @@ export class OrcaRuntimeWithRuntimeId { protected readonly terminalWait = new RuntimeTerminalWaitController( { + getScreenReadiness: (ptyId, text) => this.getTerminalScreenReadiness(ptyId, text), defaultTimeoutMs: TUI_IDLE_DEFAULT_TIMEOUT_MS, getLivePty: (handle) => this.getLivePtyForHandle(handle), getLiveLeaf: (handle) => this.getLiveLeafForHandle(handle), diff --git a/src/main/runtime/orca-runtime-visible-snapshot-preview.ts b/src/main/runtime/orca-runtime-visible-snapshot-preview.ts index ac9eb99a67b..67142a6795a 100644 --- a/src/main/runtime/orca-runtime-visible-snapshot-preview.ts +++ b/src/main/runtime/orca-runtime-visible-snapshot-preview.ts @@ -9,9 +9,38 @@ import { } from './orca-runtime-postlude' import { projectTerminalVisibleLines } from './orca-runtime-terminal-projection' import { HeadlessEmulator } from '../daemon/headless-emulator' +import { + classifyTerminalScreenReadiness, + type TerminalScreenReadiness +} from './terminal-screen-readiness' import { withTimeout } from './runtime-async-boundaries' export class OrcaRuntimeWithVisibleSnapshotPreview extends OrcaRuntimeWithCaptureProviderTerminalBuffer { + protected getTerminalScreenReadiness( + ptyId: string | null | undefined, + retainedText: string + ): TerminalScreenReadiness | null { + const agent = this.getPaneAgentForTuiIdle(ptyId) + if ( + !ptyId || + (agent ? agent !== 'antigravity' : !retainedText.toLowerCase().includes('antigravity cli')) + ) { + return null + } + const cached = this.providerVisibleStateByPtyId.get(ptyId) + if ( + cached?.generation === this.getPtyLifecycleGeneration(ptyId) && + cached.sequence >= this.getPtyOutputSequence(ptyId) && + (!cached.headlessWriteChain || + cached.headlessWriteChain === this.headlessTerminals.get(ptyId)?.writeChain) + ) { + return classifyTerminalScreenReadiness({ tail: cached.lines, draft: cached.draft }) + } + // A pending or unreachable screen cannot prove that typing is safe. + void this.readVisibleTerminalState(ptyId).catch(() => {}) + return { ready: false, blockedReason: null } + } + protected async visibleSnapshotPreview(ptyId: string, preview: string): Promise { const knownAlternateScreen = this.isTerminalAlternateScreen(ptyId) const providerModeUnknown = @@ -39,11 +68,22 @@ export class OrcaRuntimeWithVisibleSnapshotPreview extends OrcaRuntimeWithCaptur return pending.promise } let entry: { generation: number; promise: Promise } - const promise = this.loadVisibleTerminalState(ptyId).finally(() => { - if (this.providerVisibleStateReadsByPtyId.get(ptyId) === entry) { - this.providerVisibleStateReadsByPtyId.delete(ptyId) - } - }) + const promise = this.loadVisibleTerminalState(ptyId) + .then((state) => { + if ( + state && + state.generation === this.getPtyLifecycleGeneration(ptyId) && + state.sequence >= this.getPtyOutputSequence(ptyId) + ) { + this.providerVisibleStateByPtyId.set(ptyId, state) + } + return state + }) + .finally(() => { + if (this.providerVisibleStateReadsByPtyId.get(ptyId) === entry) { + this.providerVisibleStateReadsByPtyId.delete(ptyId) + } + }) entry = { generation, promise } this.providerVisibleStateReadsByPtyId.set(ptyId, entry) return promise @@ -63,6 +103,8 @@ export class OrcaRuntimeWithVisibleSnapshotPreview extends OrcaRuntimeWithCaptur if ( cached?.generation === generation && outputSequence <= cached.sequence && + (!cached.headlessWriteChain || + cached.headlessWriteChain === this.headlessTerminals.get(ptyId)?.writeChain) && (!trackedMode || trackedMode.isAlternateScreen === cached.isAlternateScreen) ) { return cached @@ -125,15 +167,18 @@ export class OrcaRuntimeWithVisibleSnapshotPreview extends OrcaRuntimeWithCaptur return null } const generation = this.getPtyLifecycleGeneration(ptyId) - await state.writeChain + const writeChain = state.writeChain + await writeChain if ( this.headlessTerminals.get(ptyId) !== state || + state.writeChain !== writeChain || this.getPtyLifecycleGeneration(ptyId) !== generation ) { return null } return { ...projectTerminalVisibleLines(state.emulator), + headlessWriteChain: writeChain, isAlternateScreen: state.emulator.isAlternateScreen, sequence: state.outputSequence, generation diff --git a/src/main/runtime/runtime-terminal-idle-polls.ts b/src/main/runtime/runtime-terminal-idle-polls.ts index e1be9654611..e2f54d1e39c 100644 --- a/src/main/runtime/runtime-terminal-idle-polls.ts +++ b/src/main/runtime/runtime-terminal-idle-polls.ts @@ -1,3 +1,4 @@ +import type { ReadTerminalScreenReadiness } from './terminal-screen-readiness' import { isShellProcess, type AgentStatus } from '../../shared/agent-detection' import type { RuntimeTerminalWait } from '../../shared/runtime-types' import { @@ -34,6 +35,7 @@ import type { TerminalWaiter } from './runtime-terminal-contracts' import type { RuntimeLeafRecord, RuntimePtyWorktreeRecord } from './runtime-terminal-state-records' type RuntimeTerminalIdlePollDependencies = { + getScreenReadiness?: ReadTerminalScreenReadiness intervalMs: number quiescenceMs: number getTabTitle(tabId: string): string | null @@ -114,7 +116,10 @@ export class RuntimeTerminalIdlePolls { let startedForegroundPoll = false try { const waitText = buildTerminalWaitText(leaf.tailBuffer, leaf.tailPartialLine, leaf.preview) - const blockedReason = detectTerminalWaitBlockedReason(waitText) + const screen = this.deps.getScreenReadiness?.(leaf.ptyId, waitText) + const blockedReason = screen + ? screen.blockedReason + : detectTerminalWaitBlockedReason(waitText) if (blockedReason) { this.stop(entry) this.deps.resolve( @@ -124,19 +129,24 @@ export class RuntimeTerminalIdlePolls { return } if ( - isTuiIdleSatisfied({ - record: leaf, - rendererTitle: leaf.paneTitle ?? this.deps.getTabTitle(leaf.tabId), - readPositiveBodyEvidence: () => isKnownReadyPromptPreview(waitText), - agent, - firstPartyStatus: this.deps.getFirstPartyAgentStatus(leaf.ptyId), - quiescenceMs: this.deps.quiescenceMs - }) + screen + ? screen.ready + : isTuiIdleSatisfied({ + record: leaf, + rendererTitle: leaf.paneTitle ?? this.deps.getTabTitle(leaf.tabId), + readPositiveBodyEvidence: () => isKnownReadyPromptPreview(waitText), + agent, + firstPartyStatus: this.deps.getFirstPartyAgentStatus(leaf.ptyId), + quiescenceMs: this.deps.quiescenceMs + }) ) { this.stop(entry) this.deps.resolve(waiter, buildTerminalWaitResult(waiter.handle, 'tui-idle', leaf)) return } + if (screen) { + return + } if ( leaf.lastAgentStatus === null && quietForegroundProcessProvesTuiIdle(agent) && @@ -180,7 +190,10 @@ export class RuntimeTerminalIdlePolls { let startedForegroundPoll = false try { const waitText = buildTerminalWaitText(pty.tailBuffer, pty.tailPartialLine, pty.preview) - const blockedReason = detectTerminalWaitBlockedReason(waitText) + const screen = this.deps.getScreenReadiness?.(pty.ptyId, waitText) + const blockedReason = screen + ? screen.blockedReason + : detectTerminalWaitBlockedReason(waitText) if (blockedReason) { this.stop(entry) this.deps.resolve( @@ -190,20 +203,25 @@ export class RuntimeTerminalIdlePolls { return } if ( - isTuiIdleSatisfied({ - record: pty, - readPositiveBodyEvidence: () => - this.deps.getAdoptedPtyIdleStatus(pty) === 'idle' || - isKnownReadyPromptPreview(waitText), - agent, - firstPartyStatus: this.deps.getFirstPartyAgentStatus(pty.ptyId), - quiescenceMs: this.deps.quiescenceMs - }) + screen + ? screen.ready + : isTuiIdleSatisfied({ + record: pty, + readPositiveBodyEvidence: () => + this.deps.getAdoptedPtyIdleStatus(pty) === 'idle' || + isKnownReadyPromptPreview(waitText), + agent, + firstPartyStatus: this.deps.getFirstPartyAgentStatus(pty.ptyId), + quiescenceMs: this.deps.quiescenceMs + }) ) { this.stop(entry) this.deps.resolve(waiter, buildPtyTerminalWaitResult(waiter.handle, 'tui-idle', pty)) return } + if (screen) { + return + } if ( pty.lastAgentStatus === null && quietForegroundProcessProvesTuiIdle(agent) && diff --git a/src/main/runtime/runtime-terminal-state-records.ts b/src/main/runtime/runtime-terminal-state-records.ts index 82e5124bf40..fae73196d3e 100644 --- a/src/main/runtime/runtime-terminal-state-records.ts +++ b/src/main/runtime/runtime-terminal-state-records.ts @@ -115,6 +115,7 @@ export type RuntimeHeadlessTerminal = { } export type RuntimeVisibleTerminalState = { + headlessWriteChain?: Promise lines: string[] draft?: string isAlternateScreen: boolean diff --git a/src/main/runtime/runtime-terminal-wait.ts b/src/main/runtime/runtime-terminal-wait.ts index cf095f85775..14acb4e35f9 100644 --- a/src/main/runtime/runtime-terminal-wait.ts +++ b/src/main/runtime/runtime-terminal-wait.ts @@ -1,3 +1,4 @@ +import type { ReadTerminalScreenReadiness } from './terminal-screen-readiness' import type { RuntimeTerminalWait as RuntimeTerminalWaitResult, RuntimeTerminalWaitCondition @@ -23,6 +24,7 @@ import type { RuntimeTerminalIdlePolls } from './runtime-terminal-idle-polls' import type { RuntimeTerminalWaiterRegistry } from './runtime-terminal-waiter-registry' type RuntimeTerminalWaitDependencies = { + getScreenReadiness?: ReadTerminalScreenReadiness defaultTimeoutMs: number getLivePty(handle: string): { pty: RuntimePtyWorktreeRecord } | null getLiveLeaf(handle: string): { leaf: RuntimeLeafRecord } @@ -44,6 +46,10 @@ export class RuntimeTerminalWait { /** Why one helper per record kind: every satisfaction site must rank the same way, * or the immediate check and the poll disagree about the same pane. */ private ptySatisfied(pty: RuntimePtyWorktreeRecord, waitText: string): boolean { + const screen = this.deps.getScreenReadiness?.(pty.ptyId, waitText) + if (screen) { + return screen.ready + } return isTuiIdleSatisfied({ record: pty, readPositiveBodyEvidence: () => @@ -55,6 +61,10 @@ export class RuntimeTerminalWait { } private leafSatisfied(leaf: RuntimeLeafRecord, waitText: string): boolean { + const screen = this.deps.getScreenReadiness?.(leaf.ptyId, waitText) + if (screen) { + return screen.ready + } return isTuiIdleSatisfied({ record: leaf, rendererTitle: leaf.paneTitle ?? this.deps.getTabTitle(leaf.tabId), @@ -65,6 +75,11 @@ export class RuntimeTerminalWait { }) } + private blockedReason(ptyId: string | null | undefined, text: string) { + const screen = this.deps.getScreenReadiness?.(ptyId, text) + return screen ? screen.blockedReason : detectTerminalWaitBlockedReason(text) + } + async wait( handle: string, options?: { @@ -84,7 +99,8 @@ export class RuntimeTerminalWait { pty.pty.tailPartialLine, pty.pty.preview ) - const ptyBlockedReason = detectTerminalWaitBlockedReason(ptyWaitText) + const ptyBlockedReason = + condition === 'tui-idle' ? this.blockedReason(pty.pty.ptyId, ptyWaitText) : null if (condition === 'tui-idle' && ptyBlockedReason) { return buildPtyTerminalWaitBlockedResult(handle, condition, pty.pty, ptyBlockedReason) } @@ -130,7 +146,7 @@ export class RuntimeTerminalWait { live.pty.tailPartialLine, live.pty.preview ) - const blockedReason = detectTerminalWaitBlockedReason(livePtyWaitText) + const blockedReason = this.blockedReason(live.pty.ptyId, livePtyWaitText) if (blockedReason) { this.waiters.resolve( waiter, @@ -153,7 +169,8 @@ export class RuntimeTerminalWait { } const leafWaitText = buildTerminalWaitText(leaf.tailBuffer, leaf.tailPartialLine, leaf.preview) - const leafBlockedReason = detectTerminalWaitBlockedReason(leafWaitText) + const leafBlockedReason = + condition === 'tui-idle' ? this.blockedReason(leaf.ptyId, leafWaitText) : null if (condition === 'tui-idle' && leafBlockedReason) { return buildTerminalWaitBlockedResult(handle, condition, leaf, leafBlockedReason) } @@ -215,7 +232,7 @@ export class RuntimeTerminalWait { live.leaf.tailPartialLine, live.leaf.preview ) - const blockedReason = detectTerminalWaitBlockedReason(liveLeafWaitText) + const blockedReason = this.blockedReason(live.leaf.ptyId, liveLeafWaitText) if (blockedReason) { this.waiters.resolve( waiter, diff --git a/src/main/runtime/terminal-screen-readiness.ts b/src/main/runtime/terminal-screen-readiness.ts index 2a21b2aa2f5..30fc8fe56f2 100644 --- a/src/main/runtime/terminal-screen-readiness.ts +++ b/src/main/runtime/terminal-screen-readiness.ts @@ -1,4 +1,8 @@ -import { isKnownReadyPromptPreview } from './terminal-wait-detection' +import { + detectTerminalWaitBlockedReason, + isKnownReadyPromptPreview +} from './terminal-wait-detection' +import type { RuntimeTerminalWaitBlockedReason } from '../../shared/runtime-types' const ANTIGRAVITY_FRAME = /^─{8,}$/ const ANTIGRAVITY_EMPTY_COMPOSERS = new Set([ @@ -25,3 +29,24 @@ export function isKnownReadyTerminalScreen(screen: { tail: string[]; draft?: str ANTIGRAVITY_FRAME.test(rows.at(-4) ?? '') ) } + +export type TerminalScreenReadiness = { + ready: boolean + blockedReason: RuntimeTerminalWaitBlockedReason | null +} + +export type ReadTerminalScreenReadiness = ( + ptyId: string | null | undefined, + retainedText: string +) => TerminalScreenReadiness | null + +export function classifyTerminalScreenReadiness(screen: { + tail: string[] + draft?: string +}): TerminalScreenReadiness { + const ready = isKnownReadyTerminalScreen(screen) + return { + ready, + blockedReason: ready ? null : detectTerminalWaitBlockedReason(screen.tail.join('\n')) + } +}