mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
fix: use current Antigravity screens for waits and delivery
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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: () =>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<string> {
|
||||
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<RuntimeVisibleTerminalState | null> }
|
||||
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
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
@@ -115,6 +115,7 @@ export type RuntimeHeadlessTerminal = {
|
||||
}
|
||||
|
||||
export type RuntimeVisibleTerminalState = {
|
||||
headlessWriteChain?: Promise<void>
|
||||
lines: string[]
|
||||
draft?: string
|
||||
isAlternateScreen: boolean
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user