Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions web/src/fixtures/panes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `╭─…─╮` / `│ ❯ … │` / `╰─ <status> ─╯`, 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.
Expand Down
21 changes: 21 additions & 0 deletions web/src/fixtures/panes/codex--v0151-draft-indented-line.txt
Original file line number Diff line number Diff line change
@@ -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
46 changes: 45 additions & 1 deletion web/src/lib/harness/codex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
];

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 12 additions & 3 deletions web/src/lib/harness/codex/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down