fix(agents): deliver grok launch drafts on its composer frame (8s → 0.7s) (#13308)
* 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.
This commit is contained in:
parent
e172a51649
commit
78f434dd85
|
|
@ -0,0 +1,124 @@
|
|||
import type { GrokStartupTraceChunk } from './grok-startup-pty-trace'
|
||||
|
||||
/**
|
||||
* Recorded PTY output of `grok --no-alt-screen`, grok's inline (non
|
||||
* alternate-screen) rendering mode — also reachable via `--minimal` and
|
||||
* `[ui] screen_mode = "minimal"` in ~/.grok/config.toml.
|
||||
*
|
||||
* Recorded from: grok 1.0.0 (3cd0d0cbcebe) [stable] on macOS (darwin 25.3.0),
|
||||
* xterm-256color 120x30 via node-pty, cwd = an Orca git worktree, 10s of output
|
||||
* from spawn.
|
||||
*
|
||||
* This mode emits NO alternate-screen switch, so the composer-glyph marker never
|
||||
* anchors and the 2004-anchored quiet window is the only delivery path. Elision
|
||||
* follows the alt-screen fixture: entries without `data` are marker-free render
|
||||
* frames kept only as timestamp + byte count.
|
||||
*/
|
||||
export const GROK_INLINE_STARTUP_PTY_TRACE: GrokStartupTraceChunk[] = [
|
||||
{
|
||||
t: 279,
|
||||
data: '\u001b]0;grok\u0007\u001b[?1000h\u001b[?1002h\u001b[?1003h\u001b[?1015h\u001b[?1006h\u001b[?1004h\u001b[?2004h\u001b[?25l\u001b]12;rgb:c8/c8'
|
||||
},
|
||||
{ t: 279, bytes: 8 },
|
||||
{ t: 2280, bytes: 11 },
|
||||
{ t: 2587, bytes: 5 },
|
||||
{ t: 2612, bytes: 825 },
|
||||
{ t: 2612, bytes: 925 },
|
||||
{ t: 2612, bytes: 928 },
|
||||
{ t: 2612, bytes: 958 },
|
||||
{ t: 2612, bytes: 810 },
|
||||
{ t: 2613, bytes: 1000 },
|
||||
{
|
||||
t: 2613,
|
||||
data: ';80;80;88;48;2;20;20;20m╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[39;48;2;20;20;20m \u001b[26;1H \u001b[38;2;80;80;88;48;2;20;20;20m│\u001b[38;2;225;225;225;48;2;20;20;20m \u001b[38;2;200;200;200;48;2;20;20;20m❯ \u001b[38;2;225;225;225;48;2;20;20;20m \u001b[38;2;80;80;88;48;2;20;20;20m│\u001b[39;48;2;20;20;20m \u001b[27;1H \u001b[38;2;80;80;88;48;2;20;20;20m╰─────────────────────────────────────────────────────────────────────────────── \u001b[38;2;128;128;128;48;2;20;20;20mGrok 4.'
|
||||
},
|
||||
{
|
||||
t: 2613,
|
||||
data: '5 (high)\u001b[38;2;88;88;88;48;2;20;20;20m · \u001b[38;2;108;108;108;48;2;20;20;20malways-approve\u001b[38;2;80;80;88;48;2;20;20;20m ─╯\u001b[39;48;2;20;20;20m \u001b[28;1H \u001b[29;1H \u001b[38;2;108;108;108;48;2;20;20;20m[stable]\u001b[39;48;2;20;20;20m \u001b[30;1H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[26;7H\u001b[?25h\u001b[?2026l'
|
||||
},
|
||||
{ t: 2614, bytes: 169 },
|
||||
{ t: 2676, bytes: 158 },
|
||||
{ t: 2697, bytes: 158 },
|
||||
{ t: 2772, bytes: 586 },
|
||||
{ t: 2781, bytes: 410 },
|
||||
{ t: 2866, bytes: 971 },
|
||||
{ t: 2866, bytes: 69 },
|
||||
{ t: 2878, bytes: 970 },
|
||||
{ t: 2878, bytes: 308 },
|
||||
{ t: 2951, bytes: 970 },
|
||||
{ t: 2951, bytes: 784 },
|
||||
{ t: 3036, bytes: 970 },
|
||||
{ t: 3036, bytes: 968 },
|
||||
{ t: 3036, bytes: 417 },
|
||||
{ t: 3121, bytes: 948 },
|
||||
{ t: 3121, bytes: 954 },
|
||||
{ t: 3121, bytes: 792 },
|
||||
{ t: 3207, bytes: 966 },
|
||||
{ t: 3207, bytes: 964 },
|
||||
{ t: 3207, bytes: 837 },
|
||||
{ t: 3291, bytes: 970 },
|
||||
{ t: 3291, bytes: 966 },
|
||||
{ t: 3291, bytes: 956 },
|
||||
{ t: 3291, bytes: 195 },
|
||||
{ t: 3376, bytes: 968 },
|
||||
{ t: 3376, bytes: 962 },
|
||||
{ t: 3376, bytes: 785 },
|
||||
{ t: 3461, bytes: 944 },
|
||||
{ t: 3461, bytes: 912 },
|
||||
{ t: 3461, bytes: 79 },
|
||||
{ t: 3546, bytes: 962 },
|
||||
{ t: 3546, bytes: 568 },
|
||||
{ t: 3630, bytes: 839 },
|
||||
{ t: 3630, bytes: 40 },
|
||||
{ t: 3715, bytes: 424 },
|
||||
{ t: 3800, bytes: 168 },
|
||||
{ t: 3885, bytes: 218 },
|
||||
{ t: 4139, bytes: 219 },
|
||||
{ t: 4394, bytes: 219 },
|
||||
{ t: 4732, bytes: 219 },
|
||||
{ t: 5582, bytes: 219 },
|
||||
{ t: 5922, bytes: 219 },
|
||||
{ t: 6177, bytes: 219 },
|
||||
{ t: 6432, bytes: 219 },
|
||||
{ t: 6602, bytes: 219 },
|
||||
{ t: 6686, bytes: 158 },
|
||||
{ t: 6771, bytes: 410 },
|
||||
{ t: 6857, bytes: 878 },
|
||||
{ t: 6857, bytes: 463 },
|
||||
{ t: 6941, bytes: 970 },
|
||||
{ t: 6941, bytes: 718 },
|
||||
{ t: 7026, bytes: 970 },
|
||||
{ t: 7026, bytes: 968 },
|
||||
{ t: 7026, bytes: 292 },
|
||||
{ t: 7110, bytes: 964 },
|
||||
{ t: 7110, bytes: 962 },
|
||||
{ t: 7110, bytes: 549 },
|
||||
{ t: 7195, bytes: 964 },
|
||||
{ t: 7195, bytes: 969 },
|
||||
{ t: 7195, bytes: 967 },
|
||||
{ t: 7195, bytes: 306 },
|
||||
{ t: 7279, bytes: 970 },
|
||||
{ t: 7279, bytes: 968 },
|
||||
{ t: 7279, bytes: 961 },
|
||||
{ t: 7279, bytes: 103 },
|
||||
{ t: 7364, bytes: 968 },
|
||||
{ t: 7364, bytes: 956 },
|
||||
{ t: 7364, bytes: 795 },
|
||||
{ t: 7449, bytes: 964 },
|
||||
{ t: 7450, bytes: 962 },
|
||||
{ t: 7450, bytes: 316 },
|
||||
{ t: 7535, bytes: 954 },
|
||||
{ t: 7535, bytes: 550 },
|
||||
{ t: 7621, bytes: 960 },
|
||||
{ t: 7621, bytes: 94 },
|
||||
{ t: 7704, bytes: 438 },
|
||||
{ t: 7789, bytes: 168 },
|
||||
{ t: 7874, bytes: 83 },
|
||||
{ t: 8043, bytes: 219 },
|
||||
{ t: 8381, bytes: 219 },
|
||||
{ t: 8635, bytes: 219 },
|
||||
{ t: 8890, bytes: 219 },
|
||||
{ t: 9144, bytes: 219 },
|
||||
{ t: 9398, bytes: 219 },
|
||||
{ t: 9738, bytes: 219 }
|
||||
]
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* Recorded PTY output of a `grok` launch, used to pin when the draft-paste
|
||||
* readiness scanner would deliver a launch draft.
|
||||
*
|
||||
* Recorded from: grok 1.0.0 (3cd0d0cbcebe) [stable] on macOS (darwin 25.3.0),
|
||||
* xterm-256color 120x30 via node-pty, cwd = an Orca git worktree, 10s of output
|
||||
* from spawn.
|
||||
*
|
||||
* Entries without `data` are grok logo-shimmer render frames, verified to hold
|
||||
* none of DECSET 1049h / 2004h, DECTCEM show-cursor, or the composer glyphs
|
||||
* U+276F / U+203A. Only their timestamp and byte count are kept so the fixture
|
||||
* stays small; the replay substitutes filler of the recorded length. Timings are
|
||||
* what the scanner reacts to, so they are preserved verbatim.
|
||||
*/
|
||||
export type GrokStartupTraceChunk = {
|
||||
/** ms after spawn */
|
||||
t: number
|
||||
data?: string
|
||||
/** Length of an elided, marker-free animation frame. */
|
||||
bytes?: number
|
||||
}
|
||||
|
||||
export const GROK_STARTUP_PTY_TRACE: GrokStartupTraceChunk[] = [
|
||||
{
|
||||
t: 274,
|
||||
data: '\u001b]0;grok\u0007\u001b[?1049h\u001b[?1000h\u001b[?1002h\u001b[?1003h\u001b[?1015h\u001b[?1006h\u001b[?1004h\u001b[?2004h\u001b[?25l'
|
||||
},
|
||||
{ t: 274, bytes: 18 },
|
||||
{ t: 582, bytes: 5 },
|
||||
{ t: 607, bytes: 825 },
|
||||
{ t: 607, bytes: 925 },
|
||||
{ t: 607, bytes: 928 },
|
||||
{ t: 607, bytes: 958 },
|
||||
{ t: 607, bytes: 810 },
|
||||
{ t: 607, bytes: 1000 },
|
||||
{
|
||||
t: 607,
|
||||
data: ';80;80;88;48;2;20;20;20m╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[39;48;2;20;20;20m \u001b[26;1H \u001b[38;2;80;80;88;48;2;20;20;20m│\u001b[38;2;225;225;225;48;2;20;20;20m \u001b[38;2;200;200;200;48;2;20;20;20m❯ \u001b[38;2;225;225;225;48;2;20;20;20m \u001b[38;2;80;80;88;48;2;20;20;20m│\u001b[39;48;2;20;20;20m \u001b[27;1H \u001b[38;2;80;80;88;48;2;20;20;20m╰─────────────────────────────────────────────────────────────────────────────── \u001b[38;2;128;128;128;48;2;20;20;20mGrok 4.'
|
||||
},
|
||||
{
|
||||
t: 607,
|
||||
data: '5 (high)\u001b[38;2;88;88;88;48;2;20;20;20m · \u001b[38;2;108;108;108;48;2;20;20;20malways-approve\u001b[38;2;80;80;88;48;2;20;20;20m ─╯\u001b[39;48;2;20;20;20m \u001b[28;1H \u001b[29;1H \u001b[38;2;108;108;108;48;2;20;20;20m[stable]\u001b[39;48;2;20;20;20m \u001b[30;1H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[26;7H\u001b[?25h\u001b[?2026l'
|
||||
},
|
||||
{ t: 613, bytes: 169 },
|
||||
{ t: 664, bytes: 158 },
|
||||
{ t: 691, bytes: 158 },
|
||||
{ t: 762, bytes: 410 },
|
||||
{ t: 776, bytes: 586 },
|
||||
{ t: 861, bytes: 971 },
|
||||
{ t: 861, bytes: 69 },
|
||||
{ t: 945, bytes: 970 },
|
||||
{ t: 945, bytes: 784 },
|
||||
{ t: 1031, bytes: 970 },
|
||||
{ t: 1031, bytes: 968 },
|
||||
{ t: 1031, bytes: 417 },
|
||||
{ t: 1109, bytes: 948 },
|
||||
{ t: 1110, bytes: 954 },
|
||||
{ t: 1110, bytes: 792 },
|
||||
{ t: 1115, bytes: 972 },
|
||||
{ t: 1115, bytes: 968 },
|
||||
{ t: 1115, bytes: 336 },
|
||||
{ t: 1200, bytes: 964 },
|
||||
{ t: 1200, bytes: 964 },
|
||||
{ t: 1200, bytes: 962 },
|
||||
{ t: 1200, bytes: 62 },
|
||||
{ t: 1286, bytes: 970 },
|
||||
{ t: 1286, bytes: 966 },
|
||||
{ t: 1286, bytes: 956 },
|
||||
{ t: 1286, bytes: 195 },
|
||||
{ t: 1370, bytes: 968 },
|
||||
{ t: 1370, bytes: 962 },
|
||||
{ t: 1370, bytes: 785 },
|
||||
{ t: 1455, bytes: 944 },
|
||||
{ t: 1455, bytes: 912 },
|
||||
{ t: 1455, bytes: 79 },
|
||||
{ t: 1540, bytes: 962 },
|
||||
{ t: 1540, bytes: 568 },
|
||||
{ t: 1625, bytes: 839 },
|
||||
{ t: 1625, bytes: 40 },
|
||||
{ t: 1710, bytes: 424 },
|
||||
{ t: 1795, bytes: 168 },
|
||||
{ t: 1879, bytes: 218 },
|
||||
{ t: 2134, bytes: 219 },
|
||||
{ t: 2389, bytes: 219 },
|
||||
{ t: 2728, bytes: 219 },
|
||||
{ t: 3579, bytes: 219 },
|
||||
{ t: 3918, bytes: 219 },
|
||||
{ t: 4172, bytes: 219 },
|
||||
{ t: 4425, bytes: 219 },
|
||||
{ t: 4594, bytes: 219 },
|
||||
{ t: 4680, bytes: 158 },
|
||||
{ t: 4764, bytes: 410 },
|
||||
{ t: 4850, bytes: 878 },
|
||||
{ t: 4850, bytes: 463 },
|
||||
{ t: 4934, bytes: 970 },
|
||||
{ t: 4934, bytes: 718 },
|
||||
{ t: 5019, bytes: 970 },
|
||||
{ t: 5019, bytes: 968 },
|
||||
{ t: 5019, bytes: 292 },
|
||||
{ t: 5103, bytes: 964 },
|
||||
{ t: 5103, bytes: 962 },
|
||||
{ t: 5103, bytes: 549 },
|
||||
{ t: 5188, bytes: 964 },
|
||||
{ t: 5188, bytes: 969 },
|
||||
{ t: 5188, bytes: 967 },
|
||||
{ t: 5188, bytes: 306 },
|
||||
{ t: 5272, bytes: 970 },
|
||||
{ t: 5272, bytes: 968 },
|
||||
{ t: 5272, bytes: 961 },
|
||||
{ t: 5272, bytes: 103 },
|
||||
{ t: 5358, bytes: 968 },
|
||||
{ t: 5358, bytes: 956 },
|
||||
{ t: 5358, bytes: 795 },
|
||||
{ t: 5442, bytes: 964 },
|
||||
{ t: 5442, bytes: 962 },
|
||||
{ t: 5442, bytes: 316 },
|
||||
{ t: 5527, bytes: 954 },
|
||||
{ t: 5527, bytes: 550 },
|
||||
{ t: 5612, bytes: 960 },
|
||||
{ t: 5612, bytes: 94 },
|
||||
{ t: 5696, bytes: 438 },
|
||||
{ t: 5781, bytes: 168 },
|
||||
{ t: 5866, bytes: 83 },
|
||||
{ t: 6120, bytes: 219 },
|
||||
{ t: 6375, bytes: 219 },
|
||||
{ t: 6631, bytes: 219 },
|
||||
{ t: 6884, bytes: 219 },
|
||||
{ t: 7139, bytes: 219 },
|
||||
{ t: 7393, bytes: 219 },
|
||||
{ t: 7732, bytes: 219 },
|
||||
{ t: 8580, bytes: 219 },
|
||||
{ t: 8664, bytes: 158 },
|
||||
{ t: 8750, bytes: 410 },
|
||||
{ t: 8835, bytes: 796 },
|
||||
{ t: 8919, bytes: 908 },
|
||||
{ t: 8919, bytes: 861 },
|
||||
{ t: 9004, bytes: 966 },
|
||||
{ t: 9004, bytes: 894 },
|
||||
{ t: 9090, bytes: 970 },
|
||||
{ t: 9090, bytes: 968 },
|
||||
{ t: 9090, bytes: 768 },
|
||||
{ t: 9175, bytes: 964 },
|
||||
{ t: 9175, bytes: 969 },
|
||||
{ t: 9175, bytes: 967 },
|
||||
{ t: 9175, bytes: 306 },
|
||||
{ t: 9260, bytes: 970 },
|
||||
{ t: 9260, bytes: 968 },
|
||||
{ t: 9260, bytes: 958 },
|
||||
{ t: 9260, bytes: 291 },
|
||||
{ t: 9344, bytes: 960 },
|
||||
{ t: 9344, bytes: 956 },
|
||||
{ t: 9344, bytes: 605 },
|
||||
{ t: 9430, bytes: 952 },
|
||||
{ t: 9430, bytes: 936 },
|
||||
{ t: 9430, bytes: 377 },
|
||||
{ t: 9515, bytes: 960 },
|
||||
{ t: 9515, bytes: 775 },
|
||||
{ t: 9600, bytes: 914 },
|
||||
{ t: 9600, bytes: 427 },
|
||||
{ t: 9684, bytes: 618 },
|
||||
{ t: 9770, bytes: 180 },
|
||||
{ t: 9855, bytes: 219 }
|
||||
]
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
// Replays recorded grok startup PTY streams through the readiness scanner and
|
||||
// asserts when each signal would have delivered the launch draft.
|
||||
//
|
||||
// The bug this covers: grok shimmers its welcome logo at ~12fps from startup
|
||||
// until the session opens, so the default quiet window (1.5s of silence after
|
||||
// DECSET 2004) never settles. Draft delivery fell through to the caller's 8s
|
||||
// hard timeout, which is why pasting a GitHub issue URL into a fresh grok
|
||||
// worktree felt frozen next to Claude's argv prefill.
|
||||
//
|
||||
// Two traces, because grok has two rendering modes and the fix must not trade
|
||||
// one for the other: the default alternate-screen mode (fast marker path) and
|
||||
// the inline mode that emits no alt-screen switch (quiet-window floor).
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createDraftPasteReadyScanner } from './draft-paste-ready-scanner'
|
||||
import type { DraftPasteReadySignal } from './tui-agent-config'
|
||||
import {
|
||||
GROK_STARTUP_PTY_TRACE,
|
||||
type GrokStartupTraceChunk
|
||||
} from './__fixtures__/grok-startup-pty-trace'
|
||||
import { GROK_INLINE_STARTUP_PTY_TRACE } from './__fixtures__/grok-inline-startup-pty-trace'
|
||||
|
||||
const QUIET_WINDOW_MS = 1500
|
||||
const HARD_TIMEOUT_MS = 8000
|
||||
|
||||
function chunkData(chunk: GrokStartupTraceChunk): string {
|
||||
return chunk.data ?? 'x'.repeat(chunk.bytes ?? 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replay `trace` against `signal` and return the ms offset at which the caller
|
||||
* would have pasted — the marker frame, or the first quiet window that elapses
|
||||
* without another chunk. `null` means the caller's hard timeout wins, which for
|
||||
* the main-process path means the draft is dropped entirely.
|
||||
*
|
||||
* Mirrors the callers in agent-draft-readiness.ts and orca-runtime.ts: the quiet
|
||||
* timer is re-armed on every chunk that asks for it, a chunk arriving before the
|
||||
* deadline cancels it, and the hard timeout outranks both — a marker that lands
|
||||
* after it is too late for the real waiters, which have already settled.
|
||||
*/
|
||||
function replayReadyAtMs(
|
||||
signal: DraftPasteReadySignal,
|
||||
trace: GrokStartupTraceChunk[]
|
||||
): number | null {
|
||||
const scanner = createDraftPasteReadyScanner(signal)
|
||||
let quietDeadline: number | null = null
|
||||
for (const [index, chunk] of trace.entries()) {
|
||||
const settledAt =
|
||||
quietDeadline !== null ? Math.min(quietDeadline, HARD_TIMEOUT_MS) : HARD_TIMEOUT_MS
|
||||
if (chunk.t >= settledAt) {
|
||||
return quietDeadline !== null && quietDeadline <= HARD_TIMEOUT_MS ? quietDeadline : null
|
||||
}
|
||||
const scanned = scanner.observe(chunkData(chunk))
|
||||
if (scanned.ready) {
|
||||
return chunk.t
|
||||
}
|
||||
quietDeadline = scanned.armQuietTimer ? chunk.t + QUIET_WINDOW_MS : quietDeadline
|
||||
if (index === trace.length - 1 && quietDeadline !== null && quietDeadline < HARD_TIMEOUT_MS) {
|
||||
return quietDeadline
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
describe('grok startup trace replay (alternate-screen mode)', () => {
|
||||
it('delivers on the composer frame instead of waiting out the hard timeout', () => {
|
||||
const readyAt = replayReadyAtMs('grok-composer-prompt', GROK_STARTUP_PTY_TRACE)
|
||||
expect(readyAt).not.toBeNull()
|
||||
expect(readyAt).toBeLessThan(1000)
|
||||
})
|
||||
|
||||
it('never settles the quiet window under the shimmering logo (the old behavior)', () => {
|
||||
// The recording runs 10s past launch; the default signal reaches the end
|
||||
// still waiting, so delivery only happened at the caller's 8s hard timeout.
|
||||
expect(replayReadyAtMs('render-quiet-after-bracketed-paste', GROK_STARTUP_PTY_TRACE)).toBeNull()
|
||||
})
|
||||
|
||||
it('fires on the same frame that paints the composer box', () => {
|
||||
const composerFrame = GROK_STARTUP_PTY_TRACE.find((chunk) => chunk.data?.includes('❯'))
|
||||
expect(composerFrame).toBeDefined()
|
||||
expect(replayReadyAtMs('grok-composer-prompt', GROK_STARTUP_PTY_TRACE)).toBe(composerFrame?.t)
|
||||
})
|
||||
})
|
||||
|
||||
describe('grok startup trace replay (inline mode, no alternate screen)', () => {
|
||||
it('still delivers through the quiet window when the marker never anchors', () => {
|
||||
// Why: inline grok emits no \x1b[?1049h, so the composer glyph never counts.
|
||||
// Anchoring the quiet window on the alt-screen switch too would leave this
|
||||
// launch with NO delivery path — orca-runtime drops the draft when readiness
|
||||
// resolves null, so the issue URL would vanish instead of arriving late.
|
||||
const readyAt = replayReadyAtMs('grok-composer-prompt', GROK_INLINE_STARTUP_PTY_TRACE)
|
||||
expect(readyAt).not.toBeNull()
|
||||
expect(readyAt).toBeLessThan(HARD_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
it('matches the default signal exactly, so inline launches keep their old timing', () => {
|
||||
expect(replayReadyAtMs('grok-composer-prompt', GROK_INLINE_STARTUP_PTY_TRACE)).toBe(
|
||||
replayReadyAtMs('render-quiet-after-bracketed-paste', GROK_INLINE_STARTUP_PTY_TRACE)
|
||||
)
|
||||
})
|
||||
|
||||
it('reports the hard timeout, not a late marker, once the waiters have settled', () => {
|
||||
// Guards the model above: the real waiters resolve at 8s, so a marker landing
|
||||
// after that must not be reported as a delivery time.
|
||||
const lateMarker: GrokStartupTraceChunk[] = [
|
||||
{ t: 0, data: '\x1b[?1049h' },
|
||||
{ t: 8500, data: '\x1b[38;2;200;200;200m❯ ' }
|
||||
]
|
||||
expect(replayReadyAtMs('grok-composer-prompt', lateMarker)).toBeNull()
|
||||
})
|
||||
|
||||
it('records a startup with no alternate-screen switch', () => {
|
||||
// Guards the fixture itself: if a future re-recording captures alt-screen
|
||||
// output, the inline assertions above would silently stop testing inline.
|
||||
expect(GROK_INLINE_STARTUP_PTY_TRACE.some((chunk) => chunk.data?.includes('\x1b[?1049h'))).toBe(
|
||||
false
|
||||
)
|
||||
expect(GROK_INLINE_STARTUP_PTY_TRACE.some((chunk) => chunk.data?.includes('❯'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -5,6 +5,9 @@ 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)', () => {
|
||||
|
|
@ -119,6 +122,120 @@ describe('createDraftPasteReadyScanner', () => {
|
|||
})
|
||||
})
|
||||
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -12,6 +12,65 @@ const CODEX_COMPOSER_PROMPT = '›'
|
|||
// racing the composer mount under slow/noisy startup. mimo-code uses the same
|
||||
// signal by parity; the quiet-window fallback covers any agent that differs.
|
||||
const DECTCEM_SHOW_CURSOR = '\x1b[?25h'
|
||||
// Why: grok's composer prompt glyph (U+276F), rendered once the input box
|
||||
// mounts. It is also the default glyph of popular shell prompts (starship,
|
||||
// pure), so it is anchored on the alternate-screen switch below — the shell
|
||||
// prompt that precedes the launch command is always in the normal buffer.
|
||||
// grok swaps it for `> ` on legacy Windows consoles, which is too generic to
|
||||
// match; those fall back to the quiet window and the caller's hard timeout.
|
||||
const GROK_COMPOSER_PROMPT = '❯'
|
||||
const DECSET_ALT_SCREEN = '\x1b[?1049h'
|
||||
const DECRST_ALT_SCREEN = '\x1b[?1049l'
|
||||
|
||||
type DraftPasteReadySignalSpec = {
|
||||
/** Bytes that must precede `marker` for it to count; null when there is no marker. */
|
||||
markerAnchor: string | null
|
||||
/** Bytes that revoke `markerAnchor` again, for anchors that describe a mode the agent can leave. */
|
||||
markerAnchorEnd: string | null
|
||||
/** Composer-ready marker, or null for signals that only use the quiet window. */
|
||||
marker: string | null
|
||||
/** Bytes that arm the quiet-window fallback, or null when the signal has none. */
|
||||
quietAnchor: string | null
|
||||
}
|
||||
|
||||
const DRAFT_PASTE_READY_SIGNALS: Record<DraftPasteReadySignal, DraftPasteReadySignalSpec> = {
|
||||
'codex-composer-prompt': {
|
||||
markerAnchor: DECSET_BRACKETED_PASTE,
|
||||
markerAnchorEnd: null,
|
||||
marker: CODEX_COMPOSER_PROMPT,
|
||||
quietAnchor: null
|
||||
},
|
||||
'render-cursor-after-bracketed-paste': {
|
||||
markerAnchor: DECSET_BRACKETED_PASTE,
|
||||
markerAnchorEnd: null,
|
||||
marker: DECTCEM_SHOW_CURSOR,
|
||||
quietAnchor: null
|
||||
},
|
||||
'grok-composer-prompt': {
|
||||
markerAnchor: DECSET_ALT_SCREEN,
|
||||
// Why: leaving the alternate screen hands the terminal back to the shell, whose
|
||||
// prompt may be `❯`. Without revoking the anchor, a grok that entered the alt
|
||||
// screen and then died — or a pager run from the user's shell rc before grok even
|
||||
// launched — would leave the glyph armed forever and paste into the shell.
|
||||
markerAnchorEnd: DECRST_ALT_SCREEN,
|
||||
marker: GROK_COMPOSER_PROMPT,
|
||||
// Why: the quiet window stays on DECSET 2004, independent of the alt-screen
|
||||
// marker anchor. grok can be configured to render inline (`--no-alt-screen`,
|
||||
// `[ui] screen_mode = "minimal"`), where 1049h never arrives — anchoring the
|
||||
// fallback there too would leave the draft with no delivery path at all, and
|
||||
// the main-process caller drops the draft when readiness never resolves.
|
||||
quietAnchor: DECSET_BRACKETED_PASTE
|
||||
},
|
||||
'render-quiet-after-bracketed-paste': {
|
||||
markerAnchor: null,
|
||||
markerAnchorEnd: null,
|
||||
marker: null,
|
||||
quietAnchor: DECSET_BRACKETED_PASTE
|
||||
}
|
||||
}
|
||||
|
||||
/** Longest anchor sequence minus one — the carry needed to rejoin one split across chunks. */
|
||||
const ANCHOR_CARRY_CHARS = 7
|
||||
|
||||
export type DraftPasteReadyScanResult = {
|
||||
/** The agent-specific ready signal fired — caller should deliver the paste now. */
|
||||
|
|
@ -37,58 +96,118 @@ export type DraftPasteReadyScanResult = {
|
|||
* during that gap and pre-empt the marker. opencode re-emits show-cursor on
|
||||
* every render frame once mounted, so the marker is effectively guaranteed;
|
||||
* the caller's hard timeout is the backstop if it never appears.
|
||||
* - `grok-composer-prompt`: ready when grok's `❯` glyph renders after the
|
||||
* alternate-screen switch (`\x1b[?1049h`). grok shimmers its startup logo
|
||||
* until the session opens, so the quiet window alone never settles and the
|
||||
* draft waited out the full hard timeout (~8s). The glyph is anchored on the
|
||||
* alt-screen switch rather than DECSET 2004 because the shell that runs the
|
||||
* launch command emits 2004 too and its own prompt may be `❯` (starship,
|
||||
* pure) — anchoring there could paste into the shell. This is the only
|
||||
* signal with both a marker and a quiet window, and they use DIFFERENT
|
||||
* anchors: grok can render inline (`--no-alt-screen`, `[ui] screen_mode =
|
||||
* "minimal"`) and on legacy Windows consoles draws `> ` instead of `❯`, so
|
||||
* the marker is best-effort and the 2004-anchored quiet window is the floor
|
||||
* that keeps those launches on the pre-existing delivery path. The alt-screen
|
||||
* anchor is revoked on `\x1b[?1049l`: leaving it hands the terminal back to
|
||||
* the shell, so a glyph after that is the shell's prompt, not grok's composer.
|
||||
* - `render-quiet-after-bracketed-paste` (default): no signal marker; arms the
|
||||
* quiet window once DECSET 2004 is seen.
|
||||
*
|
||||
* A 512-byte ring (`recent` / `postHandshakeRecent`) covers escape sequences
|
||||
* A 512-byte ring (`recent` / `postAnchorRecent`) covers escape sequences
|
||||
* split across chunk boundaries without retaining terminal scrollback.
|
||||
*/
|
||||
export function createDraftPasteReadyScanner(readySignal: DraftPasteReadySignal): {
|
||||
observe: (data: string) => DraftPasteReadyScanResult
|
||||
} {
|
||||
let recent = ''
|
||||
let postHandshakeRecent = ''
|
||||
let saw2004 = false
|
||||
let postAnchorRecent = ''
|
||||
let anchorCarry = ''
|
||||
let sawMarkerAnchor = false
|
||||
let sawQuietAnchor = false
|
||||
|
||||
const signalMarker =
|
||||
readySignal === 'codex-composer-prompt'
|
||||
? CODEX_COMPOSER_PROMPT
|
||||
: readySignal === 'render-cursor-after-bracketed-paste'
|
||||
? DECTCEM_SHOW_CURSOR
|
||||
: null
|
||||
const {
|
||||
markerAnchor,
|
||||
markerAnchorEnd,
|
||||
marker: signalMarker,
|
||||
quietAnchor
|
||||
} = DRAFT_PASTE_READY_SIGNALS[readySignal]
|
||||
|
||||
/**
|
||||
* Why: an anchor the agent can leave (the alternate screen) has to be tracked in
|
||||
* stream ORDER, not as "seen once". Walk the chunk segment by segment so a marker
|
||||
* only counts while the anchor is actually held, and re-entering re-arms it.
|
||||
* Only reachable for signals that define `markerAnchorEnd`.
|
||||
*/
|
||||
const scanRevocableAnchorSegments = (window: string, anchor: string, end: string): boolean => {
|
||||
let cursor = 0
|
||||
while (cursor < window.length) {
|
||||
if (!sawMarkerAnchor) {
|
||||
const enterIndex = window.indexOf(anchor, cursor)
|
||||
if (enterIndex === -1) {
|
||||
return false
|
||||
}
|
||||
sawMarkerAnchor = true
|
||||
postAnchorRecent = ''
|
||||
cursor = enterIndex + anchor.length
|
||||
continue
|
||||
}
|
||||
const leaveIndex = window.indexOf(end, cursor)
|
||||
const segment = leaveIndex === -1 ? window.slice(cursor) : window.slice(cursor, leaveIndex)
|
||||
if ((postAnchorRecent + segment).includes(signalMarker ?? '')) {
|
||||
return true
|
||||
}
|
||||
if (leaveIndex === -1) {
|
||||
postAnchorRecent = (postAnchorRecent + segment).slice(-512)
|
||||
return false
|
||||
}
|
||||
sawMarkerAnchor = false
|
||||
postAnchorRecent = ''
|
||||
cursor = leaveIndex + end.length
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
observe(data: string): DraftPasteReadyScanResult {
|
||||
const combined = recent + data
|
||||
recent = combined.slice(-512)
|
||||
if (!saw2004) {
|
||||
const markerIndex = combined.indexOf(DECSET_BRACKETED_PASTE)
|
||||
if (markerIndex === -1) {
|
||||
return { ready: false, armQuietTimer: false }
|
||||
}
|
||||
saw2004 = true
|
||||
const postHandshakeChunk = combined.slice(markerIndex + DECSET_BRACKETED_PASTE.length)
|
||||
if (signalMarker !== null && postHandshakeChunk.includes(signalMarker)) {
|
||||
return { ready: true, armQuietTimer: false }
|
||||
}
|
||||
postHandshakeRecent = postHandshakeChunk.slice(-512)
|
||||
} else {
|
||||
if (
|
||||
signalMarker !== null &&
|
||||
(data.includes(signalMarker) || (postHandshakeRecent + data).includes(signalMarker))
|
||||
) {
|
||||
return { ready: true, armQuietTimer: false }
|
||||
}
|
||||
postHandshakeRecent = (postHandshakeRecent + data).slice(-512)
|
||||
if (!sawQuietAnchor && quietAnchor !== null && combined.includes(quietAnchor)) {
|
||||
sawQuietAnchor = true
|
||||
}
|
||||
// Why: marker-based signals (Codex glyph, opencode show-cursor) must NOT
|
||||
// arm the quiet window. opencode goes silent for ~1.5-2s between enabling
|
||||
// bracketed paste and mounting its composer, so a quiet window would fire
|
||||
// during that gap — before the composer exists — and pre-empt the marker.
|
||||
// These signals wait for their marker, bounded only by the caller's hard
|
||||
// timeout (and the caller's best-effort process-ownership paste after it).
|
||||
// Only the default signal, which has no marker, uses the quiet window.
|
||||
return { ready: false, armQuietTimer: signalMarker === null && saw2004 }
|
||||
if (signalMarker !== null && markerAnchor !== null) {
|
||||
if (markerAnchorEnd !== null) {
|
||||
// Why: carry only the bytes an anchor could straddle, so already-scanned
|
||||
// output is never re-walked into a second enter/leave transition.
|
||||
const window = anchorCarry + data
|
||||
anchorCarry = window.slice(-ANCHOR_CARRY_CHARS)
|
||||
if (scanRevocableAnchorSegments(window, markerAnchor, markerAnchorEnd)) {
|
||||
return { ready: true, armQuietTimer: false }
|
||||
}
|
||||
} else if (!sawMarkerAnchor) {
|
||||
const anchorIndex = combined.indexOf(markerAnchor)
|
||||
if (anchorIndex !== -1) {
|
||||
sawMarkerAnchor = true
|
||||
const postAnchorChunk = combined.slice(anchorIndex + markerAnchor.length)
|
||||
if (postAnchorChunk.includes(signalMarker)) {
|
||||
return { ready: true, armQuietTimer: false }
|
||||
}
|
||||
postAnchorRecent = postAnchorChunk.slice(-512)
|
||||
}
|
||||
} else {
|
||||
if (data.includes(signalMarker) || (postAnchorRecent + data).includes(signalMarker)) {
|
||||
return { ready: true, armQuietTimer: false }
|
||||
}
|
||||
postAnchorRecent = (postAnchorRecent + data).slice(-512)
|
||||
}
|
||||
}
|
||||
// Why: the Codex glyph and opencode show-cursor signals must NOT arm the
|
||||
// quiet window (they carry no quiet anchor). opencode goes silent for
|
||||
// ~1.5-2s between enabling bracketed paste and mounting its composer, so a
|
||||
// quiet window would fire during that gap — before the composer exists —
|
||||
// and pre-empt the marker. Those signals wait for their marker, bounded
|
||||
// only by the caller's hard timeout (and its best-effort
|
||||
// process-ownership paste after that).
|
||||
return { ready: false, armQuietTimer: sawQuietAnchor }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export type DraftPasteReadySignal =
|
|||
| 'render-quiet-after-bracketed-paste'
|
||||
| 'codex-composer-prompt'
|
||||
| 'render-cursor-after-bracketed-paste'
|
||||
| 'grok-composer-prompt'
|
||||
|
||||
export type TuiAgentDetectionRuntime = NodeJS.Platform | 'wsl'
|
||||
|
||||
|
|
@ -302,6 +303,10 @@ export const TUI_AGENT_CONFIG: Record<TuiAgent, TuiAgentConfig> = {
|
|||
promptInjectionMode: 'argv',
|
||||
// Why: separator so prompts like `help`/`--version` aren't parsed as Grok CLI syntax.
|
||||
argvPromptSeparator: '--',
|
||||
// Why: grok shimmers its startup logo until the session opens, so the quiet
|
||||
// window never settles and launch drafts waited out the full 8s hard
|
||||
// timeout; its composer glyph lands ~0.6s in.
|
||||
draftPasteReadySignal: 'grok-composer-prompt',
|
||||
ctrlEnterEncoding: 'csi-u'
|
||||
},
|
||||
devin: {
|
||||
|
|
|
|||
|
|
@ -778,6 +778,23 @@ describe('tui agent startup plans', () => {
|
|||
).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps grok on the composer-glyph paste draft route', () => {
|
||||
// Why: grok has no --prefill-style flag, so every launch draft goes through
|
||||
// paste-after-ready — and its shimmering startup logo never settles the
|
||||
// quiet window, which is what made the paste take the full hard timeout.
|
||||
expect(TUI_AGENT_CONFIG.grok.draftPasteReadySignal).toBe('grok-composer-prompt')
|
||||
expect(TUI_AGENT_CONFIG.grok.draftPromptFlag).toBeUndefined()
|
||||
expect(TUI_AGENT_CONFIG.grok.draftPromptEnvVar).toBeUndefined()
|
||||
expect(
|
||||
buildAgentDraftLaunchPlan({
|
||||
agent: 'grok',
|
||||
draft: 'x',
|
||||
cmdOverrides: {},
|
||||
platform: 'darwin'
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('appends Kiro trust defaults to the chat subcommand that accepts them', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'kiro',
|
||||
|
|
|
|||
Loading…
Reference in New Issue