diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index f3d12467e45..9096c3c7a4c 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -418,7 +418,7 @@ import { } from '../../shared/tui-agent-config' import { createDraftPasteReadyScanner, - draftPasteReadyBudgetMs + DRAFT_PASTE_READY_TIMEOUT_MS } from '../../shared/draft-paste-ready-scanner' import { detectInstalledAgentsWithShellPathHydration, detectRemoteAgents } from '../ipc/preflight' import { @@ -21374,7 +21374,7 @@ export class OrcaRuntimeService { // delivery paths do, instead of silently dropping the prompt (STA-3367). hardTimer = setTimeout(() => { void this.resolveDraftPastePtyIfAgentOwns(ptyId, agent).then(finish) - }, draftPasteReadyBudgetMs(readySignal)) + }, DRAFT_PASTE_READY_TIMEOUT_MS) }) } diff --git a/src/main/runtime/startup-draft-paste-readiness.test.ts b/src/main/runtime/startup-draft-paste-readiness.test.ts index d1068af7a35..8a7a75eb212 100644 --- a/src/main/runtime/startup-draft-paste-readiness.test.ts +++ b/src/main/runtime/startup-draft-paste-readiness.test.ts @@ -6,18 +6,16 @@ * scanner, the shared budget, and the fallback are all exercised. */ import { beforeEach, afterEach, describe, expect, it, vi, type Mock } from 'vitest' -import { draftPasteReadyBudgetMs } from '../../shared/draft-paste-ready-scanner' +import { DRAFT_PASTE_READY_TIMEOUT_MS } from '../../shared/draft-paste-ready-scanner' import { OrcaRuntimeService } from './orca-runtime' const DECSET_BRACKETED_PASTE = '\x1b[?2004h' const CODEX_COMPOSER_PROMPT_RENDER = '\x1b[1m›\x1b[0m Ask Codex to do anything' const PTY_ID = 'pty-1' const HANDLE = 'term-1' -// Why: pull the budgets from the shared policy rather than restating them, so -// this fails if the runtime waiter ever stops consuming it. The literal values -// are pinned once, in draft-paste-ready-scanner.test.ts. -const CODEX_MARKER_BUDGET_MS = draftPasteReadyBudgetMs('codex-composer-prompt') -const QUIET_WINDOW_BUDGET_MS = draftPasteReadyBudgetMs('render-quiet-after-bracketed-paste') +// Why: the old budget that dropped the prompt, kept only to prove the waiter now +// outlasts it. The current value is pinned in draft-paste-ready-scanner.test.ts. +const OLD_DROPPED_AT_MS = 8000 type RuntimeHost = { getLivePtyForHandle: (handle: string) => { pty: { ptyId: string } } | null @@ -80,18 +78,18 @@ describe('waitForStartupDraftReady', () => { expect(vi.getTimerCount()).toBe(0) }) - it('waits the full marker budget before giving up on a cold codex composer', async () => { + it('waits the full budget before giving up on a cold codex composer', async () => { const promise = waitForStartupDraftReady.call(host, HANDLE, 'codex') await Promise.resolve() observer?.(DECSET_BRACKETED_PASTE) - // The old markerless-length budget would already have resolved (to null) here. - await vi.advanceTimersByTimeAsync(QUIET_WINDOW_BUDGET_MS) + // The old budget would already have resolved (to null) here. + await vi.advanceTimersByTimeAsync(OLD_DROPPED_AT_MS) expect(getForegroundProcess).not.toHaveBeenCalled() - // Still inside the marker budget: a late glyph is the real ready proof, and - // must win over the ownership fallback. - await vi.advanceTimersByTimeAsync(CODEX_MARKER_BUDGET_MS - QUIET_WINDOW_BUDGET_MS - 1) + // Still inside the budget: a late glyph is the real ready proof, and must + // win over the ownership fallback. + await vi.advanceTimersByTimeAsync(DRAFT_PASTE_READY_TIMEOUT_MS - OLD_DROPPED_AT_MS - 1) observer?.(CODEX_COMPOSER_PROMPT_RENDER) await expect(promise).resolves.toBe(PTY_ID) expect(getForegroundProcess).not.toHaveBeenCalled() @@ -105,7 +103,7 @@ describe('waitForStartupDraftReady', () => { const promise = waitForStartupDraftReady.call(host, HANDLE, 'codex') await Promise.resolve() - await vi.advanceTimersByTimeAsync(CODEX_MARKER_BUDGET_MS) + await vi.advanceTimersByTimeAsync(DRAFT_PASTE_READY_TIMEOUT_MS) await expect(promise).resolves.toBe(PTY_ID) expect(getForegroundProcess).toHaveBeenCalledWith(PTY_ID) @@ -117,7 +115,7 @@ describe('waitForStartupDraftReady', () => { const promise = waitForStartupDraftReady.call(host, HANDLE, 'codex') await Promise.resolve() - await vi.advanceTimersByTimeAsync(CODEX_MARKER_BUDGET_MS) + await vi.advanceTimersByTimeAsync(DRAFT_PASTE_READY_TIMEOUT_MS) await expect(promise).resolves.toBeNull() }) @@ -127,7 +125,7 @@ describe('waitForStartupDraftReady', () => { const promise = waitForStartupDraftReady.call(host, HANDLE, 'codex') await Promise.resolve() - await vi.advanceTimersByTimeAsync(CODEX_MARKER_BUDGET_MS) + await vi.advanceTimersByTimeAsync(DRAFT_PASTE_READY_TIMEOUT_MS) await expect(promise).resolves.toBeNull() }) diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index 08d51421bec..5a234abe835 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -306,7 +306,7 @@ import { isWslUncPath } from '../../../../shared/wsl-paths' import { isTuiAgent, TUI_AGENT_CONFIG } from '../../../../shared/tui-agent-config' import { createDraftPasteReadyScanner, - draftPasteReadyBudgetMs + DRAFT_PASTE_READY_TIMEOUT_MS } from '../../../../shared/draft-paste-ready-scanner' import { sendAgentDraftPasteContent } from '@/lib/agent-draft-paste-content' import { writeTerminalPastePtyInput } from './terminal-pty-paste-writer' @@ -4883,7 +4883,7 @@ export function connectPanePty( startupDraftHardTimer = setTimeout(() => { startupDraftHardTimer = null void deliverStartupDraftIfAgentOwnsPty() - }, draftPasteReadyBudgetMs(startupDraftReadySignal)) + }, DRAFT_PASTE_READY_TIMEOUT_MS) } const armStartupDraftQuietTimer = (): void => { if (!startupDraftReadyScanner || startupDraftPasteSettled) { diff --git a/src/renderer/src/lib/agent-paste-draft.ts b/src/renderer/src/lib/agent-paste-draft.ts index 8901ee19512..989c59f6228 100644 --- a/src/renderer/src/lib/agent-paste-draft.ts +++ b/src/renderer/src/lib/agent-paste-draft.ts @@ -1,6 +1,6 @@ import type { TuiAgent, GlobalSettings } from '../../../shared/types' import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' -import { draftPasteReadyBudgetMs } from '../../../shared/draft-paste-ready-scanner' +import { DRAFT_PASTE_READY_TIMEOUT_MS } from '../../../shared/draft-paste-ready-scanner' import { useAppStore } from '@/store' import { inspectRuntimeTerminalProcess, @@ -68,8 +68,7 @@ export function getSettingsForAgentTabRuntimeOwner( * Returns true when the paste was issued, false on timeout or missing * PTY. `onTimeout` lets the caller surface a UI hint (e.g. toast) when * the agent doesn't reach a ready state. `timeoutMs` overrides the - * per-signal readiness budget only; waiting for the PTY to spawn keeps its - * own fixed budget. + * readiness budget only; waiting for the PTY to spawn keeps its own budget. * * Readiness combines DECSET 2004 with one agent-specific follow-up signal: * 1. `\x1b[?2004h` (DECSET 2004 — bracketed-paste-enable) on the PTY @@ -110,7 +109,7 @@ export async function pasteDraftWhenAgentReady(args: { const settings = getSettingsForAgentTabRuntimeOwner(tabId) // Why: the readiness budget starts once the PTY exists, so a slow spawn // shortens nothing — a cold composer still gets its full window. - const budget = timeoutMs ?? draftPasteReadyBudgetMs(readySignal) + const budget = timeoutMs ?? DRAFT_PASTE_READY_TIMEOUT_MS const ready = await waitForAgentDraftInputReady(ptyId, budget, readySignal, settings) if (!ready) { // Why: fast-starting TUIs can emit the paste-ready escape sequence before @@ -153,7 +152,7 @@ export async function pasteDraftToAgentPtyWhenReady(args: { const settings = getSettingsForAgentTabRuntimeOwner(tabId) const readySignal = agentConfig?.draftPasteReadySignal ?? 'render-quiet-after-bracketed-paste' - const budget = timeoutMs ?? draftPasteReadyBudgetMs(readySignal) + const budget = timeoutMs ?? DRAFT_PASTE_READY_TIMEOUT_MS const ready = await waitForAgentDraftInputReady(ptyId, budget, readySignal, settings) if (!ready) { const fallbackReady = agentConfig diff --git a/src/shared/draft-paste-ready-scanner.test.ts b/src/shared/draft-paste-ready-scanner.test.ts index 9f715b173f4..dbcad3d0035 100644 --- a/src/shared/draft-paste-ready-scanner.test.ts +++ b/src/shared/draft-paste-ready-scanner.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest' -import { createDraftPasteReadyScanner, draftPasteReadyBudgetMs } from './draft-paste-ready-scanner' +import { + createDraftPasteReadyScanner, + DRAFT_PASTE_READY_TIMEOUT_MS +} from './draft-paste-ready-scanner' const DECSET_BRACKETED_PASTE = '\x1b[?2004h' const SHOW_CURSOR = '\x1b[?25h' @@ -137,28 +140,10 @@ describe('createDraftPasteReadyScanner', () => { }) }) -describe('draftPasteReadyBudgetMs', () => { - it('gives marker-gated signals room for a cold boot (STA-3367)', () => { - // Why: the marker is a positive proof, so waiting longer can only tolerate a - // slow first-run codex — it can never paste into a composer that isn't there. - expect(draftPasteReadyBudgetMs('codex-composer-prompt')).toBe(20000) - expect(draftPasteReadyBudgetMs('render-cursor-after-bracketed-paste')).toBe(20000) - }) - - it('keeps the markerless quiet window tight', () => { - // Why: silence is a guess, not a proof; a longer budget just delays a wrong answer. - expect(draftPasteReadyBudgetMs('render-quiet-after-bracketed-paste')).toBe(8000) - }) - - it('gives every marker-gated signal a strictly larger budget than the quiet window', () => { - // Guards the invariant rather than the numbers: a new marker signal added to - // the scanner must not silently inherit the tighter markerless budget. - const quiet = draftPasteReadyBudgetMs('render-quiet-after-bracketed-paste') - for (const signal of [ - 'codex-composer-prompt', - 'render-cursor-after-bracketed-paste' - ] as const) { - expect(draftPasteReadyBudgetMs(signal)).toBeGreaterThan(quiet) - } +describe('DRAFT_PASTE_READY_TIMEOUT_MS', () => { + it('leaves room for a cold-boot composer (STA-3367)', () => { + // Why: the reported drop was a first-run codex composer mounting past the old + // 8s budget. Pinned once here; every delivery path imports rather than restates it. + expect(DRAFT_PASTE_READY_TIMEOUT_MS).toBe(20000) }) }) diff --git a/src/shared/draft-paste-ready-scanner.ts b/src/shared/draft-paste-ready-scanner.ts index 6708ad956af..3693925d360 100644 --- a/src/shared/draft-paste-ready-scanner.ts +++ b/src/shared/draft-paste-ready-scanner.ts @@ -20,32 +20,21 @@ export type DraftPasteReadyScanResult = { armQuietTimer: boolean } -// Why: the markerless quiet-window signal guesses readiness from output silence, -// so a long budget just delays a wrong answer. Keep it tight. -const QUIET_WINDOW_READY_TIMEOUT_MS = 8000 -// Why: marker-gated signals (Codex '›', opencode show-cursor) are positive -// readiness proofs — the paste fires only when the marker actually renders, so a -// longer budget can never paste prematurely; it only tolerates a slow cold boot. -// First-run codex on a cold app can take >8s to mount its composer, past which -// the old budget gave up and dropped the launch prompt (STA-3367). -const MARKER_READY_TIMEOUT_MS = 20000 - /** - * How long a delivery path should wait for `readySignal` before falling back. + * Backstop for every draft-paste delivery path: how long to wait for a ready + * signal before falling back to a best-effort, ownership-checked paste. * - * Lives beside the scanner because the budget is a property of the signal — only - * this module knows which signals are marker-gated — not of the calling path. All - * three draft-delivery owners (renderer tab paste, renderer startup paste, main - * runtime startup paste) consume this so the policy cannot drift between them. + * One budget for all signals. The value is a property of how slowly an agent can + * boot, not of how readiness is detected — a marker, a quiet window, and a + * process check all wait on the same cold start. First-run codex on a cold app + * can take >8s to mount its composer, past which the old budget gave up and + * dropped the launch prompt (STA-3367). * - * This is a backstop, not a correctness gate: every caller must still attempt a - * best-effort delivery when the agent owns the PTY after the budget expires. + * Lives beside the scanner so all three delivery owners (renderer tab paste, + * renderer startup paste, main runtime startup paste) share one policy and + * cannot drift apart. */ -export function draftPasteReadyBudgetMs(readySignal: DraftPasteReadySignal): number { - return readySignal === 'render-quiet-after-bracketed-paste' - ? QUIET_WINDOW_READY_TIMEOUT_MS - : MARKER_READY_TIMEOUT_MS -} +export const DRAFT_PASTE_READY_TIMEOUT_MS = 20000 /** * Pure, incremental scanner shared by the renderer and main-process draft-paste