diff --git a/web/src/fixtures/panes/README.md b/web/src/fixtures/panes/README.md index 5dac0a68..a5284d8e 100644 --- a/web/src/fixtures/panes/README.md +++ b/web/src/fixtures/panes/README.md @@ -69,6 +69,20 @@ model turn could be run. | `codex--v0150-custom-status.txt` | `-c 'tui.status_line=["model-with-reasoning","current-dir","git-branch"]'` (Context deliberately omitted) with a short draft on the `› ` row. Pins the styled custom-status design: per-field colours and dim ` · ` separators | `idle` | | `codex--v0150-paste-placeholder.txt` | One `pane.send_text` of exactly 3000 non-newline ASCII characters lands as `[Pasted Content 1024 chars]` — **N is 1024, not 3000**. Codex's TUI keeps only the first 1024 characters of a single burst, so `draftCarriesSend("x".repeat(3000), draft)` is **false** (it is true for a 1024-character send). See `codex/paste.ts` | `idle` | +## Codex 0.151.0 capture (2026-08-29, herdr 0.8.2, Linux sandbox pane) + +Byte-faithful `format:ansi` capture from a throwaway herdr tab in `/tmp/collie-codex-sandbox`, with +one length-preserving sanitization pass on the shell prompt the pane opened with (`user@sbox`, `$`). +Status row is 0.150.1's two-field default, so the styled acceptor is what locates the composer here +too. **The headline: a continuation row is NOT always two-space indented.** The gutter is two +spaces, but what follows it is the operator's own text — and a draft carrying a hard line break +whose next line begins with spaces (one shift+enter, trivial to type on a phone) paints a FOUR-space +row. That is an ordinary draft, not a dialog. + +| Fixture | State / what's in it | Herdr status | +|---|---|---| +| `codex--v0151-draft-indented-line.txt` | Two-line draft: the `› ` row, then a hard line break whose text starts with two spaces, painted as a four-space-indented continuation above the two-field status row. `composerReady` must be TRUE — `/^ {2}\S/` refused it, `locateComposer` returned null, and the pane refused every send with "the agent's input box isn't on screen" until the draft was cleared | `idle` | + ## Grok corpus (live panes 2026-08-21–23) Grok's composer is a rounded box at the tail: `╭─…─╮` / `│ ❯ … │` / `╰─ ─╯`, then a blank and a key-hint row. The status run is opaque (display name, optional effort, optional permission mode). User-message bubbles use **square** corners (`┌ ┐ └ ┘`) and must never be read as the composer. **All identifying content genericized** per the repo's public-repo rule. diff --git a/web/src/fixtures/panes/codex--v0151-draft-indented-line.txt b/web/src/fixtures/panes/codex--v0151-draft-indented-line.txt new file mode 100644 index 00000000..e4ebb97c --- /dev/null +++ b/web/src/fixtures/panes/codex--v0151-draft-indented-line.txt @@ -0,0 +1,21 @@ +[user@sbox collie-codex-sandbox]$ codex + +╭────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.151.0) │ +│ │ +│ model: gpt-5.6-sol medium /model to change │ +│ directory: /tmp/collie-codex-sandbox │ +│ permissions: YOLO mode │ +╰────────────────────────────────────────────────────╯ + + Tip: Use /compact when the conversation gets long to + summarize history and free up context. + +• You have 1 usage limit reset available. Run /usage to use +one. + + +› please move all the images across to the new blog + then take the originals down once the copy is verified + + gpt-5.6-sol medium · /tmp/collie-codex-sandbox \ No newline at end of file diff --git a/web/src/lib/harness/codex.test.ts b/web/src/lib/harness/codex.test.ts index ef1402a8..9cfdda52 100644 --- a/web/src/lib/harness/codex.test.ts +++ b/web/src/lib/harness/codex.test.ts @@ -42,6 +42,7 @@ const PINNED = [ "codex--v0150-idle.txt", "codex--v0150-nogit-idle.txt", "codex--v0150-paste-placeholder.txt", + "codex--v0151-draft-indented-line.txt", "codex--working.txt", ]; @@ -76,7 +77,13 @@ function fixtureLines(name: string) { } describe("composerReady — the gate the reply path pre-flights on", () => { - it.each(["codex--fresh-idle.txt", "codex--draft.txt", "codex--draft-wrapped.txt", "codex--working.txt"])( + it.each([ + "codex--fresh-idle.txt", + "codex--draft.txt", + "codex--draft-wrapped.txt", + "codex--v0151-draft-indented-line.txt", + "codex--working.txt", + ])( "%s: the composer is on screen ⇒ true", (name) => { expect(codexAdapter.composerReady!(fixtureLines(name))).toBe(true); @@ -184,6 +191,43 @@ describe("chrome", () => { expect(codexAdapter.extractInputDraft(lines)).toBe("a message waiting to send"); }); + it("a wrapped row whose own text starts with spaces is still a continuation", () => { + // The shape, pinned without a capture: two spaces of gutter, then the operator's own text, + // which may itself begin with spaces. `codex--v0151-draft-indented-line.txt` below is the + // real render of it and carries the reasoning. + const screen = [ + "\u203a move everything across including the images and", + " then take the originals down", + "", + " gpt-5.6-sol high · /home/user · Context 50% left", + ].join("\n"); + const lines = splitLines(parseAnsi(screen)); + + expect(locateComposer(lines)).not.toBeNull(); + expect(codexAdapter.composerReady!(lines)).toBe(true); + expect(codexAdapter.extractInputDraft(lines)).toBe( + "move everything across including the images and then take the originals down", + ); + }); + + it("locates a draft whose continuation row is indented deeper than the gutter", () => { + // The gutter is two spaces; what FOLLOWS it is the operator's own text, and that text may + // itself begin with spaces. This capture is the everyday way it happens: a draft carrying a + // hard line break (shift+enter, one tap on a phone keyboard) whose next line starts with two + // spaces paints a FOUR-space continuation row. `/^ {2}\\S/` demanded a non-space at column 2, + // read that healthy row as foreign, and locateComposer returned null — so the pane refused + // EVERY send with "the agent's input box isn't on screen" for as long as the draft sat there. + // A deadlock, not a transient: the refusal is itself what keeps the draft from being sent, so + // the pane never recovers on its own. + const lines = fixtureLines("codex--v0151-draft-indented-line.txt"); + + expect(locateComposer(lines)).not.toBeNull(); + expect(codexAdapter.composerReady!(lines)).toBe(true); + expect(codexAdapter.extractInputDraft(lines)).toBe( + "please move all the images across to the new blog then take the originals down once the copy is verified", + ); + }); + it("a draft that wraps past 8 rows is still a composer", () => { // The old bound of 8 stranded a phone wrap: locateComposer returned null and the pane // reported a dialog. 1 prompt + 8 continuations is 9 rows, the first case that failed. diff --git a/web/src/lib/harness/codex/chrome.ts b/web/src/lib/harness/codex/chrome.ts index 55930762..fe878b06 100644 --- a/web/src/lib/harness/codex/chrome.ts +++ b/web/src/lib/harness/codex/chrome.ts @@ -31,9 +31,18 @@ export interface ComposerBox { // Claude. A run deeper than this is not a composer (fail closed — locateComposer returns null). const MAX_DRAFT_ROWS = 100; -// Continuation rows are exactly two-space-indented text. Deeper indents belong to dialogs and -// transcript blocks; a `› ` or `• ` row is never a continuation. -const CONTINUATION = /^ {2}\S/; +// A continuation row is the composer's TWO-SPACE GUTTER followed by the draft's own text — and +// that text may ITSELF begin with spaces. Type two spaces mid-sentence, or let a soft wrap land +// inside a run of them, and Codex paints a four-space-indented row that is a perfectly healthy +// continuation. The old `/^ {2}\S/` demanded a non-space at column 2, read that row as foreign, +// and made `locateComposer` return null — which refused EVERY send in the pane with "the input +// box isn't on screen — a menu or dialog is probably up" for as long as the draft sat there. That +// is a DEADLOCK, not a transient: the refusal is itself what keeps the draft from being sent, so +// the pane never recovers on its own. Only the gutter is asserted here, because only the gutter is +// the renderer's; what the walk actually bounds the run with is the blank row above it (`isBlank`, +// checked first in the same test), and Codex separates every section of a screen with one. A `› ` +// or `• ` row still starts at column 0, so neither can pass as a continuation. +const CONTINUATION = /^ {2}\s*\S/; const PROMPT_PREFIX = "› "; /** The exact placeholder text is still a valid thing an operator might deliberately type. Codex