mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 08:02:33 +00:00
* fix(agents): deliver grok launch drafts on its composer frame
Grok has no --prefill-style flag, so a launch draft (e.g. the issue URL of
a worktree created from a GitHub issue) always goes through Orca's
paste-after-ready path. That path used the default readiness signal: DECSET
2004 plus 1.5s of PTY silence. Grok shimmers its startup logo at ~12fps
until the session opens, so the quiet window never settled and the draft
fell through to the 8s hard timeout before it appeared in the composer.
Gate grok on its own composer glyph instead, anchored on the alternate-screen
switch rather than DECSET 2004: the shell that runs the launch command emits
2004 too, and its prompt may itself be the same glyph (starship, pure), so a
Codex-style anchor could paste into the shell. Grok keeps the quiet window
armed as a fallback because it renders differentially and paints the glyph
once, so a late-attaching scanner would otherwise wait out the hard timeout.
Measured against grok 1.0.0 driving the real scanner over a zsh -> grok PTY:
draft delivery moves from 8003ms to 689ms, with the URL landing unsubmitted
in the composer exactly as before.
* fix(agents): keep grok's quiet-window floor on DECSET 2004
The composer-glyph marker is anchored on the alternate-screen switch, but grok
can render inline (`--no-alt-screen`, `--minimal`, `[ui] screen_mode =
"minimal"`), where 1049h never arrives. Anchoring the quiet-window fallback
there too left those launches with no delivery path at all: readiness never
resolved, and the main-process caller drops the draft when it resolves null —
so the issue URL vanished instead of arriving late.
Give the signal two independent anchors: the marker still waits for the
alt-screen switch (so a starship/pure shell prompt can't trip it), while the
quiet window arms off DECSET 2004 exactly as the default signal does. Inline and
legacy-Windows-console launches keep their pre-existing timing; alt-screen
launches keep the fast marker path.
Verified on grok 1.0.0 over a real zsh -> grok PTY: alt-screen delivers at 687ms
via the marker, inline at 1949ms via the quiet window (the default signal
measures 1861ms on the same launch), URL landing unsubmitted in both. Adds a
recorded inline-mode trace fixture so the no-1049h path stays covered.
* fix(agents): revoke grok's alt-screen anchor when the screen is handed back
The composer-glyph anchor latched forever: once \x1b[?1049h had been seen, any
later `❯` counted as grok's composer. Two ways that pastes the launch draft into
the user's shell instead of into grok:
- grok enters the alternate screen and then dies before painting a composer;
the shell prompt that follows is `❯` under starship or pure.
- a pager or editor started from the user's shell rc enters and leaves the
alternate screen before grok is ever launched, arming the anchor against the
shell's own prompt.
Track the anchor in stream order instead of as a latch: \x1b[?1049l revokes it,
re-entering re-arms it, and a marker only counts inside a segment where the
anchor is actually held. The chunk is walked segment by segment so ordering
within a single PTY packet is honored, with a 7-char carry — one short of the
escape sequence — so a split sequence rejoins without re-walking scanned output
into a second transition. Signals with no `markerAnchorEnd` (codex, opencode,
the default) keep their existing latch semantics untouched.
Also makes the trace-replay test model the hard timeout: the real waiters settle
at 8s, so a marker landing after that is not a delivery time.
256 lines
13 KiB
TypeScript
256 lines
13 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import { createDraftPasteReadyScanner } from './draft-paste-ready-scanner'
|
||
|
||
const DECSET_BRACKETED_PASTE = '\x1b[?2004h'
|
||
const SHOW_CURSOR = '\x1b[?25h'
|
||
const HIDE_CURSOR = '\x1b[?25l'
|
||
const CODEX_PROMPT = '\x1b[1m›\x1b[0m Ask Codex to do anything'
|
||
const GROK_ALT_SCREEN_ENTER = '\x1b[?1049h\x1b[?2004h\x1b[?25l'
|
||
const GROK_ALT_SCREEN_LEAVE = '\x1b[?1049l\x1b[?25h'
|
||
const GROK_COMPOSER_FRAME = '\x1b[38;2;80;80;88m│\x1b[38;2;200;200;200m❯ \x1b[0m'
|
||
|
||
describe('createDraftPasteReadyScanner', () => {
|
||
describe('render-cursor-after-bracketed-paste (opencode / mimo-code)', () => {
|
||
it('is ready when show-cursor renders after bracketed paste in one chunk', () => {
|
||
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
|
||
expect(scanner.observe(`${DECSET_BRACKETED_PASTE}${SHOW_CURSOR}`)).toEqual({
|
||
ready: true,
|
||
armQuietTimer: false
|
||
})
|
||
})
|
||
|
||
it('does not fire on bracketed paste alone, then fires once show-cursor arrives', () => {
|
||
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
|
||
// Why: opencode enables bracketed paste ~1.5-2s before its composer mounts
|
||
// and stays SILENT in between. The cursor gates delivery and must NOT arm
|
||
// the quiet window, which would otherwise fire during that silent gap and
|
||
// paste before the composer exists.
|
||
expect(scanner.observe(DECSET_BRACKETED_PASTE)).toEqual({
|
||
ready: false,
|
||
armQuietTimer: false
|
||
})
|
||
expect(scanner.observe('startup banner output')).toEqual({
|
||
ready: false,
|
||
armQuietTimer: false
|
||
})
|
||
expect(scanner.observe(SHOW_CURSOR)).toEqual({ ready: true, armQuietTimer: false })
|
||
})
|
||
|
||
it('resolves from a single replayed buffer holding both markers (SSH/remote replay path)', () => {
|
||
// Why: the runtime waiter feeds recentPtyOutputById as one observe() call
|
||
// when the agent emitted 2004 + show-cursor before the subscription
|
||
// attached; a single combined buffer must still resolve.
|
||
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
|
||
expect(
|
||
scanner.observe(`banner\n${DECSET_BRACKETED_PASTE}composer\n${SHOW_CURSOR}rest`)
|
||
).toEqual({ ready: true, armQuietTimer: false })
|
||
})
|
||
|
||
it('detects a bracketed-paste handshake split across a chunk boundary', () => {
|
||
// Why: the pre-handshake `recent` ring must reassemble a \x1b[?2004h that
|
||
// straddles two PTY packets, or cursor-gated readiness breaks for
|
||
// fragmented startup output.
|
||
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
|
||
expect(scanner.observe('\x1b[?20')).toEqual({ ready: false, armQuietTimer: false })
|
||
expect(scanner.observe('04h')).toEqual({ ready: false, armQuietTimer: false })
|
||
expect(scanner.observe(SHOW_CURSOR)).toEqual({ ready: true, armQuietTimer: false })
|
||
})
|
||
|
||
it('detects show-cursor split across a later chunk boundary', () => {
|
||
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
|
||
scanner.observe(DECSET_BRACKETED_PASTE)
|
||
// The escape sequence is split mid-bytes across two separate chunks.
|
||
expect(scanner.observe('render noise \x1b[?')).toEqual({ ready: false, armQuietTimer: false })
|
||
expect(scanner.observe('25h')).toEqual({ ready: true, armQuietTimer: false })
|
||
})
|
||
|
||
it('never arms the quiet window during the silent pre-composer gap', () => {
|
||
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
|
||
scanner.observe(DECSET_BRACKETED_PASTE)
|
||
// Why: opencode is silent here; arming the quiet window would fire before
|
||
// the composer mounts and pre-empt the cursor signal (the original bug).
|
||
// Delivery waits for show-cursor, bounded by the caller's hard timeout.
|
||
for (let i = 0; i < 5; i += 1) {
|
||
expect(scanner.observe(`setup output ${i}`)).toEqual({ ready: false, armQuietTimer: false })
|
||
}
|
||
})
|
||
|
||
it('does not treat hide-cursor as the ready signal', () => {
|
||
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
|
||
// \x1b[?25l (hide) must not be mistaken for \x1b[?25h (show).
|
||
expect(scanner.observe(`${DECSET_BRACKETED_PASTE}${HIDE_CURSOR}`)).toEqual({
|
||
ready: false,
|
||
armQuietTimer: false
|
||
})
|
||
})
|
||
|
||
it('ignores show-cursor that appears before bracketed paste is enabled', () => {
|
||
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
|
||
// A pre-handshake cursor toggle must not trip readiness.
|
||
expect(scanner.observe(SHOW_CURSOR)).toEqual({ ready: false, armQuietTimer: false })
|
||
expect(scanner.observe(DECSET_BRACKETED_PASTE)).toEqual({
|
||
ready: false,
|
||
armQuietTimer: false
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('codex-composer-prompt (unchanged behavior)', () => {
|
||
it('is ready on the composer glyph after bracketed paste and never arms the quiet timer', () => {
|
||
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
|
||
expect(scanner.observe(DECSET_BRACKETED_PASTE)).toEqual({
|
||
ready: false,
|
||
armQuietTimer: false
|
||
})
|
||
expect(scanner.observe(CODEX_PROMPT)).toEqual({ ready: true, armQuietTimer: false })
|
||
})
|
||
|
||
it('detects the composer glyph inside a large first render chunk', () => {
|
||
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
|
||
expect(scanner.observe(`${DECSET_BRACKETED_PASTE}${CODEX_PROMPT}${'x'.repeat(900)}`)).toEqual(
|
||
{ ready: true, armQuietTimer: false }
|
||
)
|
||
})
|
||
|
||
it('never arms the quiet-window fallback', () => {
|
||
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
|
||
expect(scanner.observe(DECSET_BRACKETED_PASTE)).toEqual({
|
||
ready: false,
|
||
armQuietTimer: false
|
||
})
|
||
expect(scanner.observe('noise')).toEqual({ ready: false, armQuietTimer: false })
|
||
})
|
||
})
|
||
|
||
describe('grok-composer-prompt', () => {
|
||
it('is ready on the composer glyph after the alternate-screen switch', () => {
|
||
const scanner = createDraftPasteReadyScanner('grok-composer-prompt')
|
||
expect(scanner.observe(GROK_ALT_SCREEN_ENTER)).toEqual({ ready: false, armQuietTimer: true })
|
||
expect(scanner.observe(GROK_COMPOSER_FRAME)).toEqual({ ready: true, armQuietTimer: false })
|
||
})
|
||
|
||
it('ignores a shell prompt glyph emitted before grok takes the screen', () => {
|
||
// Why: `❯` is starship's / pure's default prompt too, and that prompt —
|
||
// with its own DECSET 2004 — renders in the normal buffer while the shell
|
||
// still owns the PTY. Firing there would paste the draft into the shell.
|
||
// The shell's 2004 still arms the quiet floor, exactly as it does today
|
||
// for every agent on the default signal.
|
||
const scanner = createDraftPasteReadyScanner('grok-composer-prompt')
|
||
expect(scanner.observe(`${DECSET_BRACKETED_PASTE}\x1b[32m❯\x1b[0m grok\r\n`)).toEqual({
|
||
ready: false,
|
||
armQuietTimer: true
|
||
})
|
||
expect(scanner.observe(GROK_ALT_SCREEN_ENTER)).toEqual({ ready: false, armQuietTimer: true })
|
||
expect(scanner.observe(GROK_COMPOSER_FRAME)).toEqual({ ready: true, armQuietTimer: false })
|
||
})
|
||
|
||
it('resolves from a single replayed buffer holding both markers (SSH/remote replay path)', () => {
|
||
const scanner = createDraftPasteReadyScanner('grok-composer-prompt')
|
||
expect(scanner.observe(`${GROK_ALT_SCREEN_ENTER}logo frames${GROK_COMPOSER_FRAME}`)).toEqual({
|
||
ready: true,
|
||
armQuietTimer: false
|
||
})
|
||
})
|
||
|
||
it('keeps arming the quiet window so a missed composer frame still delivers', () => {
|
||
// Why: grok renders differentially — the glyph is painted once, so a
|
||
// scanner that attached after that frame would otherwise wait out the
|
||
// caller's hard timeout. Output only goes quiet once startup settles.
|
||
const scanner = createDraftPasteReadyScanner('grok-composer-prompt')
|
||
scanner.observe(GROK_ALT_SCREEN_ENTER)
|
||
expect(scanner.observe('logo shimmer frame')).toEqual({ ready: false, armQuietTimer: true })
|
||
})
|
||
|
||
it('arms the quiet window from DECSET 2004 when grok renders inline', () => {
|
||
// Why: `--no-alt-screen` / `[ui] screen_mode = "minimal"` emits no 1049h,
|
||
// so the glyph never anchors. The quiet window must still arm off 2004 or
|
||
// readiness never resolves and the main-process caller drops the draft.
|
||
const scanner = createDraftPasteReadyScanner('grok-composer-prompt')
|
||
expect(scanner.observe(DECSET_BRACKETED_PASTE)).toEqual({ ready: false, armQuietTimer: true })
|
||
expect(scanner.observe(GROK_COMPOSER_FRAME)).toEqual({ ready: false, armQuietTimer: true })
|
||
})
|
||
|
||
it('does not treat a legacy-console `> ` prompt as the glyph', () => {
|
||
// grok draws `> ` instead of `❯` on legacy Windows consoles; it is too
|
||
// generic to match, so those launches ride the quiet window.
|
||
const scanner = createDraftPasteReadyScanner('grok-composer-prompt')
|
||
scanner.observe(GROK_ALT_SCREEN_ENTER)
|
||
expect(scanner.observe('\x1b[38;2;80;80;88m│\x1b[0m> ')).toEqual({
|
||
ready: false,
|
||
armQuietTimer: true
|
||
})
|
||
})
|
||
|
||
it('disarms when grok leaves the alternate screen before painting a composer', () => {
|
||
// Why: grok entering the alt screen and then dying hands the terminal back to
|
||
// the shell. A latched anchor would treat the shell's `❯` prompt as grok's
|
||
// composer and paste the draft into the shell.
|
||
const scanner = createDraftPasteReadyScanner('grok-composer-prompt')
|
||
scanner.observe(GROK_ALT_SCREEN_ENTER)
|
||
expect(scanner.observe(GROK_ALT_SCREEN_LEAVE)).toEqual({
|
||
ready: false,
|
||
armQuietTimer: true
|
||
})
|
||
expect(scanner.observe(`\x1b[32m❯\x1b[0m `)).toEqual({ ready: false, armQuietTimer: true })
|
||
})
|
||
|
||
it('ignores a shell prompt after an rc-file program used the alternate screen', () => {
|
||
// Why: a pager/editor launched from the user's shell rc enters and leaves the
|
||
// alt screen before grok is even launched; the prompt that follows is the
|
||
// shell's, so the anchor must not survive the leave.
|
||
const scanner = createDraftPasteReadyScanner('grok-composer-prompt')
|
||
expect(
|
||
scanner.observe(`rc pager${GROK_ALT_SCREEN_ENTER}paged${GROK_ALT_SCREEN_LEAVE}`)
|
||
).toEqual({ ready: false, armQuietTimer: true })
|
||
expect(scanner.observe(`${DECSET_BRACKETED_PASTE}\x1b[32m❯\x1b[0m grok\r\n`)).toEqual({
|
||
ready: false,
|
||
armQuietTimer: true
|
||
})
|
||
// grok's own launch still resolves normally afterwards.
|
||
expect(scanner.observe(GROK_ALT_SCREEN_ENTER)).toEqual({ ready: false, armQuietTimer: true })
|
||
expect(scanner.observe(GROK_COMPOSER_FRAME)).toEqual({ ready: true, armQuietTimer: false })
|
||
})
|
||
|
||
it('ignores a glyph that precedes the alt-screen switch inside one chunk', () => {
|
||
const scanner = createDraftPasteReadyScanner('grok-composer-prompt')
|
||
expect(scanner.observe(`❯ ${GROK_ALT_SCREEN_ENTER}`)).toEqual({
|
||
ready: false,
|
||
armQuietTimer: true
|
||
})
|
||
})
|
||
|
||
it('does not fire on a glyph that lands after the leave inside one chunk', () => {
|
||
const scanner = createDraftPasteReadyScanner('grok-composer-prompt')
|
||
scanner.observe(GROK_ALT_SCREEN_ENTER)
|
||
expect(scanner.observe(`${GROK_ALT_SCREEN_LEAVE}\x1b[32m❯\x1b[0m `)).toEqual({
|
||
ready: false,
|
||
armQuietTimer: true
|
||
})
|
||
})
|
||
|
||
it('detects the alt-screen anchor split across a chunk boundary', () => {
|
||
const scanner = createDraftPasteReadyScanner('grok-composer-prompt')
|
||
expect(scanner.observe('\x1b[?10')).toEqual({ ready: false, armQuietTimer: false })
|
||
expect(scanner.observe('49h')).toEqual({ ready: false, armQuietTimer: false })
|
||
expect(scanner.observe(GROK_COMPOSER_FRAME)).toEqual({ ready: true, armQuietTimer: false })
|
||
})
|
||
})
|
||
|
||
describe('render-quiet-after-bracketed-paste (default)', () => {
|
||
it('arms the quiet timer after bracketed paste and never reports a signal', () => {
|
||
const scanner = createDraftPasteReadyScanner('render-quiet-after-bracketed-paste')
|
||
expect(scanner.observe(DECSET_BRACKETED_PASTE)).toEqual({ ready: false, armQuietTimer: true })
|
||
// Show-cursor is not a signal for the default path; it just keeps arming.
|
||
expect(scanner.observe(SHOW_CURSOR)).toEqual({ ready: false, armQuietTimer: true })
|
||
})
|
||
|
||
it('does nothing until bracketed paste is enabled', () => {
|
||
const scanner = createDraftPasteReadyScanner('render-quiet-after-bracketed-paste')
|
||
expect(scanner.observe('pre-handshake output')).toEqual({
|
||
ready: false,
|
||
armQuietTimer: false
|
||
})
|
||
})
|
||
})
|
||
})
|