diff --git a/web/src/components/agent-chat.test.tsx b/web/src/components/agent-chat.test.tsx index a6a8aeac..594d6a34 100644 --- a/web/src/components/agent-chat.test.tsx +++ b/web/src/components/agent-chat.test.tsx @@ -364,14 +364,15 @@ describe("AgentChat — prompt-select race guard wiring (frozen {text, revision} // The block grammars are provably scoped to the pane's own adapter (spec T8): an agent with no // adapter gets the plain raw mirror — no prompt-select buttons, no chrome stripping, no re-surfaced // status strip — because running Claude-tuned matchers on an unverified TUI could mis-lift or -// mis-strip its output. codex is such an agent; omp has an adapter but lifts no dialog kind at all. +// mis-strip its output. opencode is such an agent (codex graduated to its own adapter); omp has an +// adapter but lifts no dialog kind at all. describe("AgentChat — block-grammar scoping (an agent with no adapter)", () => { - // A codex agent sharing the Claude fixture's ids, so only the agent kind differs from the default. - const codexAgent = { ...fixtureAgents[0]!, agent: "codex" }; + // An opencode agent sharing the Claude fixture's ids, so only the agent kind differs from the default. + const opencodeAgent = { ...fixtureAgents[0]!, agent: "opencode" }; - it("does NOT lift a codex tail menu into buttons — it stays raw mirror text", () => { - renderChat({ text: MENU_TEXT, agent: codexAgent }); - // No native prompt buttons: the Claude prompt-select grammar never runs for codex… + it("does NOT lift an adapterless agent's tail menu into buttons — it stays raw mirror text", () => { + renderChat({ text: MENU_TEXT, agent: opencodeAgent }); + // No native prompt buttons: the Claude prompt-select grammar never runs without an adapter… expect(screen.queryByRole("button", { name: "Yes" })).not.toBeInTheDocument(); // …and the menu row shows verbatim in the raw mirror instead (drivable by the keys pad). expect(screen.getByText(/1\. Yes/)).toBeInTheDocument(); @@ -392,8 +393,8 @@ describe("AgentChat — block-grammar scoping (an agent with no adapter)", () => expect(screen.queryByText(/❯/)).toBeNull(); // the input box was stripped off the mirror }); - it("leaves a codex input-box buffer fully raw — no status strip, box kept in the mirror", () => { - renderChat({ text: STATUS_TEXT, agent: codexAgent }); + it("leaves an adapterless agent's input-box buffer fully raw — no status strip, box kept in the mirror", () => { + renderChat({ text: STATUS_TEXT, agent: opencodeAgent }); // The statusline is NOT hoisted into an app strip — it stays inside the raw
mirror…
const status = screen.getByText(/\[Opus 4\.8\] ~\/webapp · main/);
expect(status.closest("pre")).not.toBeNull();
diff --git a/web/src/fixtures/panes/README.md b/web/src/fixtures/panes/README.md
index 299e0484..45334a32 100644
--- a/web/src/fixtures/panes/README.md
+++ b/web/src/fixtures/panes/README.md
@@ -16,6 +16,33 @@ scripts/capture-fixture.sh [lines] # paneIds: /api/snapshot
(`less -R `) for private content before `git add` — prefer generating states in a sandbox
pane over capturing real work sessions.
+## Codex corpus (captured 2026-08-22, Codex v0.149.0, sandbox panes)
+
+Byte-faithful `format:ansi` captures with one sanitization pass, every substitution
+LENGTH-PRESERVING so row padding stays byte-identical: the operator's username and hostname
+(`collie-user`, `collies-macbook-pro-1` — painted in the shell prompt line and host config
+path, which no sandbox can avoid), the Darwin per-user temp-dir token
+(`sanitizedtempdirtoken000000000`), and the Codex session UUIDs from `codex resume` lines
+(`00000000-0000-7000-8000-…`). Codex's chrome is boxless: a
+`› ` prompt row (wrapping onto two-space-indented continuation rows) with a dot-separated status
+row beneath (` · · Context N% left · weekly N% left`); every section of a screen is
+separated by exactly one blank row. Approval dialogs need `-c approvals_reviewer=user` — with
+`auto_review` (the capture host's default) eligible requests route through a reviewer subagent
+and the observed commands were approved with no dialog painted.
+
+| Fixture | State / what's in it | Herdr status |
+|---|---|---|
+| `codex--trust-prompt.txt` | First screen in an untrusted directory: trust paragraph, `› 1. Yes, continue / 2. No, quit`, `Press enter to continue`. Digit 2 live-probed: quit immediately | `blocked` |
+| `codex--fresh-idle.txt` | Welcome banner box, tips, empty `› Ask Codex to do anything` composer, status row | `idle` |
+| `codex--draft.txt` | One-line draft on the `› ` row | `idle` |
+| `codex--draft-wrapped.txt` | Long draft word-wrapped onto a two-space-indented continuation row | `idle` |
+| `codex--working.txt` | `• Working (3s • esc to interrupt)` above a still-visible composer (Codex queues mid-turn) | `working` |
+| `codex--approval-exec.txt` | Exec approval: header, Environment/Reason, `$ command`, options `1. Yes, proceed (y)` / `2. …don't ask again… (p)` / `3. No… (esc)`, enter/esc footer. Digits 1 and 3 live-probed (1 ran the command, 3 rejected it — file verified absent); `y` probed too | `blocked` |
+| `codex--ask-fruit.txt` | `request_user_input` card: `Question 1/1` header, options with descriptions plus the auto-added `None of the above`, notes footer. Digit live-probed: answers AND submits | `blocked` |
+| `codex--ask-wizard-q1.txt` | Two-question set, `Question 1/2`; footer adds `←/→ to navigate questions`. Digit probed: answers and advances | `blocked` |
+| `codex--ask-wizard-q2.txt` | Same set, `Question 2/2`; footer `enter to submit all`. Digit probed: submits the whole set | `blocked` |
+| `codex--ask-notes-focused.txt` | Notes box open (`› Add notes`, footer `tab or esc to clear notes`): a digit would TYPE — the adapter refuses to raw | `blocked` |
+
## Corpus (captured 2026-07-04, Claude Code TUI as of that date)
| Fixture | State / what's in it | Herdr status |
diff --git a/web/src/fixtures/panes/codex--approval-exec.txt b/web/src/fixtures/panes/codex--approval-exec.txt
new file mode 100644
index 00000000..ae32b3fc
--- /dev/null
+++ b/web/src/fixtures/panes/codex--approval-exec.txt
@@ -0,0 +1,184 @@
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex
+
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m Run [0m[38;5;6mcodex app[0m to open the Desktop app (it installs on macOS if needed).
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (it is outside your sandbox, so it should need my approval). Do nothing else. [0m
+
+
+[0m[2m• [0mI’ll run exactly that command.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed with exit code 0.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,478 input=21,226 (+ 34,304 cached) output=252 (reasoning 105)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000001[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval untrusted
+[0m[1m[38;5;1merror:[0m invalid value '[0m[38;5;3muntrusted[0m' for '[0m[1m--ask-for-approval [0m'
+ [possible values: [0m[38;5;2mon-request[0m, [0m[38;5;2mnever[0m]
+
+For more information, try '[0m[1m--help[0m'.
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mI’m requesting approval to create that exact file outside the sandbox.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed; file existence was not independently verified.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,447 input=21,212 (+ 34,304 cached) output=235 (reasoning 67)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000002[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=none
+[0m[2m╭───────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0m[2m[3mloading[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0mloading[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────╯[0m
+
+[0m[48;2;65;69;76m [0m
+[0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m [0mError loading config.toml: unknown variant `none`, expected one of `user`, `auto_review`, `guardian_subagent`[0m[48;2;65;69;76m [0m
+in `approvals_reviewer`[0m[48;2;65;69;76m [0m
+ [0m[2m? for shortcuts[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=user
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mRequesting approval to run the exact command outside the sandbox.
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /Users/collie-user/codex-approval-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse the request_user_input tool ONCE: ask "Pick a fruit?" with options Apple and Pear, short descriptions. Do nothing else. [0m
+
+
+[0m[2m•[0m [0m[1mQuestions[0m[2m 1/1 answered[0m
+ • Pick a fruit?
+[0m[2m answer: [0m[38;5;6mPear[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Pear selected.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mCount slowly from 1 to 15, thinking carefully between each number. Do nothing else. [0m
+
+
+[0m[2m• [0m1
+
+ 2
+
+ 3
+
+ 4
+
+ 5
+
+ 6
+
+ 7
+
+ 8
+
+ 9
+
+ 10
+
+ 11
+
+ 12
+
+ 13
+
+ 14
+
+ 15
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /tmp/collie-codex-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[1m[38;2;255;255;255m•[0m [0m[1mRunning[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /tmp/collie-codex-probe.txt[0m
+
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[1m[48;2;65;69;76mWould you like to run the following command?[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m Environment: [0m[1m[48;2;65;69;76mlocal[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m Reason: [0m[3m[48;2;65;69;76mDo you want to allow creating /tmp/collie-codex-probe.txt outside the sandbox?[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m $ [0m[38;2;137;180;250m[48;2;65;69;76mtouch[0m[38;2;205;214;244m[48;2;65;69;76m /tmp/collie-codex-probe.txt[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[1m[38;5;6m[48;2;65;69;76m› 1. Yes, proceed (y)[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m 2. Yes, and don't ask again for commands that start with `touch /tmp/collie-codex-probe.txt` ([0m[2m[48;2;65;69;76mp[0m[48;2;65;69;76m) [0m
+[0m[48;2;65;69;76m 3. No, and tell Codex what to do differently ([0m[2m[48;2;65;69;76mesc[0m[48;2;65;69;76m) [0m
+[0m[48;2;65;69;76m [0m
+ [0m[2mPress enter to confirm or esc to cancel [0m
\ No newline at end of file
diff --git a/web/src/fixtures/panes/codex--ask-fruit.txt b/web/src/fixtures/panes/codex--ask-fruit.txt
new file mode 100644
index 00000000..820f3ef0
--- /dev/null
+++ b/web/src/fixtures/panes/codex--ask-fruit.txt
@@ -0,0 +1,132 @@
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex
+
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m Run [0m[38;5;6mcodex app[0m to open the Desktop app (it installs on macOS if needed).
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (it is outside your sandbox, so it should need my approval). Do nothing else. [0m
+
+
+[0m[2m• [0mI’ll run exactly that command.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed with exit code 0.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,478 input=21,226 (+ 34,304 cached) output=252 (reasoning 105)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000001[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval untrusted
+[0m[1m[38;5;1merror:[0m invalid value '[0m[38;5;3muntrusted[0m' for '[0m[1m--ask-for-approval [0m'
+ [possible values: [0m[38;5;2mon-request[0m, [0m[38;5;2mnever[0m]
+
+For more information, try '[0m[1m--help[0m'.
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mI’m requesting approval to create that exact file outside the sandbox.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed; file existence was not independently verified.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,447 input=21,212 (+ 34,304 cached) output=235 (reasoning 67)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000002[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=none
+[0m[2m╭───────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0m[2m[3mloading[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0mloading[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────╯[0m
+
+[0m[48;2;65;69;76m [0m
+[0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m [0mError loading config.toml: unknown variant `none`, expected one of `user`, `auto_review`, `guardian_subagent`[0m[48;2;65;69;76m [0m
+in `approvals_reviewer`[0m[48;2;65;69;76m [0m
+ [0m[2m? for shortcuts[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=user
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mRequesting approval to run the exact command outside the sandbox.
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /Users/collie-user/codex-approval-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse the request_user_input tool ONCE: ask "Pick a fruit?" with options Apple and Pear, short descriptions. Do nothing else. [0m
+
+
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[2m[48;2;65;69;76mQuestion 1/1 (1 unanswered)[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[38;5;6m[48;2;65;69;76mPick a fruit?[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[1m[38;5;6m[48;2;65;69;76m› 1. Apple (Recommended) Choose a crisp, sweet apple.[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m 2. Pear [0m[2m[48;2;65;69;76mChoose a soft, juicy pear.[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m 3. None of the above [0m[2m[48;2;65;69;76mOptionally, add details in notes (tab).[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[1m[38;5;6m[48;2;65;69;76mtab to add notes[0m[2m[48;2;65;69;76m | [0m[1m[38;5;6m[48;2;65;69;76menter to submit answer[0m[2m[48;2;65;69;76m | esc to interrupt[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
\ No newline at end of file
diff --git a/web/src/fixtures/panes/codex--ask-notes-focused.txt b/web/src/fixtures/panes/codex--ask-notes-focused.txt
new file mode 100644
index 00000000..9a57ed22
--- /dev/null
+++ b/web/src/fixtures/panes/codex--ask-notes-focused.txt
@@ -0,0 +1,203 @@
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex
+
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m Run [0m[38;5;6mcodex app[0m to open the Desktop app (it installs on macOS if needed).
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (it is outside your sandbox, so it should need my approval). Do nothing else. [0m
+
+
+[0m[2m• [0mI’ll run exactly that command.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed with exit code 0.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,478 input=21,226 (+ 34,304 cached) output=252 (reasoning 105)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000001[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval untrusted
+[0m[1m[38;5;1merror:[0m invalid value '[0m[38;5;3muntrusted[0m' for '[0m[1m--ask-for-approval [0m'
+ [possible values: [0m[38;5;2mon-request[0m, [0m[38;5;2mnever[0m]
+
+For more information, try '[0m[1m--help[0m'.
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mI’m requesting approval to create that exact file outside the sandbox.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed; file existence was not independently verified.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,447 input=21,212 (+ 34,304 cached) output=235 (reasoning 67)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000002[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=none
+[0m[2m╭───────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0m[2m[3mloading[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0mloading[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────╯[0m
+
+[0m[48;2;65;69;76m [0m
+[0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m [0mError loading config.toml: unknown variant `none`, expected one of `user`, `auto_review`, `guardian_subagent`[0m[48;2;65;69;76m [0m
+in `approvals_reviewer`[0m[48;2;65;69;76m [0m
+ [0m[2m? for shortcuts[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=user
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mRequesting approval to run the exact command outside the sandbox.
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /Users/collie-user/codex-approval-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse the request_user_input tool ONCE: ask "Pick a fruit?" with options Apple and Pear, short descriptions. Do nothing else. [0m
+
+
+[0m[2m•[0m [0m[1mQuestions[0m[2m 1/1 answered[0m
+ • Pick a fruit?
+[0m[2m answer: [0m[38;5;6mPear[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Pear selected.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mCount slowly from 1 to 15, thinking carefully between each number. Do nothing else. [0m
+
+
+[0m[2m• [0m1
+
+ 2
+
+ 3
+
+ 4
+
+ 5
+
+ 6
+
+ 7
+
+ 8
+
+ 9
+
+ 10
+
+ 11
+
+ 12
+
+ 13
+
+ 14
+
+ 15
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /tmp/collie-codex-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[38;5;1m✗ [0mYou [0m[1mcanceled[0m the request to run [0m[2mtouch /tmp/collie-codex-probe.txt[0m
+
+[0m[1m[38;5;1m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /tmp/collie-codex-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[38;5;1m■ Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue.[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command again: touch /tmp/collie-codex-probe.txt (outside your sandbox — request approval). Do nothing else. [0m
+
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /tmp/collie-codex-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /tmp/collie-codex-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse request_user_input ONCE with TWO questions in one call: "Tabs or spaces?" options Tabs, Spaces; and "Semicolons?" options Yes, No. Short descriptions. Do [0m
+[0m[48;2;65;69;76m nothing else. [0m
+
+
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[2m[48;2;65;69;76mQuestion 1/2 (2 unanswered)[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[38;5;6m[48;2;65;69;76mTabs or spaces?[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[1m[38;5;6m[48;2;65;69;76m› 1. Tabs (Recommended) Indent code with tab characters.[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m 2. Spaces [0m[2m[48;2;65;69;76mIndent code with space characters.[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m 3. None of the above [0m[2m[48;2;65;69;76mOptionally, add details in notes (tab).[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m [0m[2m[48;2;65;69;76mAdd notes[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[2m[48;2;65;69;76mtab or esc to clear notes | enter to submit answer[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
\ No newline at end of file
diff --git a/web/src/fixtures/panes/codex--ask-wizard-q1.txt b/web/src/fixtures/panes/codex--ask-wizard-q1.txt
new file mode 100644
index 00000000..27f9f30a
--- /dev/null
+++ b/web/src/fixtures/panes/codex--ask-wizard-q1.txt
@@ -0,0 +1,201 @@
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex
+
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m Run [0m[38;5;6mcodex app[0m to open the Desktop app (it installs on macOS if needed).
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (it is outside your sandbox, so it should need my approval). Do nothing else. [0m
+
+
+[0m[2m• [0mI’ll run exactly that command.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed with exit code 0.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,478 input=21,226 (+ 34,304 cached) output=252 (reasoning 105)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000001[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval untrusted
+[0m[1m[38;5;1merror:[0m invalid value '[0m[38;5;3muntrusted[0m' for '[0m[1m--ask-for-approval [0m'
+ [possible values: [0m[38;5;2mon-request[0m, [0m[38;5;2mnever[0m]
+
+For more information, try '[0m[1m--help[0m'.
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mI’m requesting approval to create that exact file outside the sandbox.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed; file existence was not independently verified.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,447 input=21,212 (+ 34,304 cached) output=235 (reasoning 67)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000002[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=none
+[0m[2m╭───────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0m[2m[3mloading[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0mloading[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────╯[0m
+
+[0m[48;2;65;69;76m [0m
+[0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m [0mError loading config.toml: unknown variant `none`, expected one of `user`, `auto_review`, `guardian_subagent`[0m[48;2;65;69;76m [0m
+in `approvals_reviewer`[0m[48;2;65;69;76m [0m
+ [0m[2m? for shortcuts[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=user
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mRequesting approval to run the exact command outside the sandbox.
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /Users/collie-user/codex-approval-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse the request_user_input tool ONCE: ask "Pick a fruit?" with options Apple and Pear, short descriptions. Do nothing else. [0m
+
+
+[0m[2m•[0m [0m[1mQuestions[0m[2m 1/1 answered[0m
+ • Pick a fruit?
+[0m[2m answer: [0m[38;5;6mPear[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Pear selected.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mCount slowly from 1 to 15, thinking carefully between each number. Do nothing else. [0m
+
+
+[0m[2m• [0m1
+
+ 2
+
+ 3
+
+ 4
+
+ 5
+
+ 6
+
+ 7
+
+ 8
+
+ 9
+
+ 10
+
+ 11
+
+ 12
+
+ 13
+
+ 14
+
+ 15
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /tmp/collie-codex-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[38;5;1m✗ [0mYou [0m[1mcanceled[0m the request to run [0m[2mtouch /tmp/collie-codex-probe.txt[0m
+
+[0m[1m[38;5;1m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /tmp/collie-codex-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[38;5;1m■ Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue.[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command again: touch /tmp/collie-codex-probe.txt (outside your sandbox — request approval). Do nothing else. [0m
+
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /tmp/collie-codex-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /tmp/collie-codex-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse request_user_input ONCE with TWO questions in one call: "Tabs or spaces?" options Tabs, Spaces; and "Semicolons?" options Yes, No. Short descriptions. Do [0m
+[0m[48;2;65;69;76m nothing else. [0m
+
+
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[2m[48;2;65;69;76mQuestion 1/2 (2 unanswered)[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[38;5;6m[48;2;65;69;76mTabs or spaces?[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[1m[38;5;6m[48;2;65;69;76m› 1. Tabs (Recommended) Indent code with tab characters.[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m 2. Spaces [0m[2m[48;2;65;69;76mIndent code with space characters.[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m 3. None of the above [0m[2m[48;2;65;69;76mOptionally, add details in notes (tab).[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[1m[38;5;6m[48;2;65;69;76mtab to add notes[0m[2m[48;2;65;69;76m | enter to submit answer | ←/→ to navigate questions | esc to interrupt[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
\ No newline at end of file
diff --git a/web/src/fixtures/panes/codex--ask-wizard-q2.txt b/web/src/fixtures/panes/codex--ask-wizard-q2.txt
new file mode 100644
index 00000000..50b102cd
--- /dev/null
+++ b/web/src/fixtures/panes/codex--ask-wizard-q2.txt
@@ -0,0 +1,201 @@
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex
+
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m Run [0m[38;5;6mcodex app[0m to open the Desktop app (it installs on macOS if needed).
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (it is outside your sandbox, so it should need my approval). Do nothing else. [0m
+
+
+[0m[2m• [0mI’ll run exactly that command.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed with exit code 0.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,478 input=21,226 (+ 34,304 cached) output=252 (reasoning 105)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000001[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval untrusted
+[0m[1m[38;5;1merror:[0m invalid value '[0m[38;5;3muntrusted[0m' for '[0m[1m--ask-for-approval [0m'
+ [possible values: [0m[38;5;2mon-request[0m, [0m[38;5;2mnever[0m]
+
+For more information, try '[0m[1m--help[0m'.
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mI’m requesting approval to create that exact file outside the sandbox.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed; file existence was not independently verified.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,447 input=21,212 (+ 34,304 cached) output=235 (reasoning 67)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000002[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=none
+[0m[2m╭───────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0m[2m[3mloading[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0mloading[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────╯[0m
+
+[0m[48;2;65;69;76m [0m
+[0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m [0mError loading config.toml: unknown variant `none`, expected one of `user`, `auto_review`, `guardian_subagent`[0m[48;2;65;69;76m [0m
+in `approvals_reviewer`[0m[48;2;65;69;76m [0m
+ [0m[2m? for shortcuts[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=user
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mRequesting approval to run the exact command outside the sandbox.
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /Users/collie-user/codex-approval-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse the request_user_input tool ONCE: ask "Pick a fruit?" with options Apple and Pear, short descriptions. Do nothing else. [0m
+
+
+[0m[2m•[0m [0m[1mQuestions[0m[2m 1/1 answered[0m
+ • Pick a fruit?
+[0m[2m answer: [0m[38;5;6mPear[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Pear selected.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mCount slowly from 1 to 15, thinking carefully between each number. Do nothing else. [0m
+
+
+[0m[2m• [0m1
+
+ 2
+
+ 3
+
+ 4
+
+ 5
+
+ 6
+
+ 7
+
+ 8
+
+ 9
+
+ 10
+
+ 11
+
+ 12
+
+ 13
+
+ 14
+
+ 15
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /tmp/collie-codex-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[38;5;1m✗ [0mYou [0m[1mcanceled[0m the request to run [0m[2mtouch /tmp/collie-codex-probe.txt[0m
+
+[0m[1m[38;5;1m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /tmp/collie-codex-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[38;5;1m■ Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue.[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command again: touch /tmp/collie-codex-probe.txt (outside your sandbox — request approval). Do nothing else. [0m
+
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /tmp/collie-codex-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /tmp/collie-codex-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse request_user_input ONCE with TWO questions in one call: "Tabs or spaces?" options Tabs, Spaces; and "Semicolons?" options Yes, No. Short descriptions. Do [0m
+[0m[48;2;65;69;76m nothing else. [0m
+
+
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[2m[48;2;65;69;76mQuestion 2/2 (1 unanswered)[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[38;5;6m[48;2;65;69;76mSemicolons?[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[1m[38;5;6m[48;2;65;69;76m› 1. Yes (Recommended) End applicable statements with semicolons.[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m 2. No [0m[2m[48;2;65;69;76mOmit optional semicolons.[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m 3. None of the above [0m[2m[48;2;65;69;76mOptionally, add details in notes (tab).[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m[1m[38;5;6m[48;2;65;69;76mtab to add notes[0m[2m[48;2;65;69;76m | [0m[1m[38;5;6m[48;2;65;69;76menter to submit all[0m[2m[48;2;65;69;76m | ←/→ to navigate questions | esc to interrupt[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
\ No newline at end of file
diff --git a/web/src/fixtures/panes/codex--draft-wrapped.txt b/web/src/fixtures/panes/codex--draft-wrapped.txt
new file mode 100644
index 00000000..a568582c
--- /dev/null
+++ b/web/src/fixtures/panes/codex--draft-wrapped.txt
@@ -0,0 +1,227 @@
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex
+
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m Run [0m[38;5;6mcodex app[0m to open the Desktop app (it installs on macOS if needed).
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (it is outside your sandbox, so it should need my approval). Do nothing else. [0m
+
+
+[0m[2m• [0mI’ll run exactly that command.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed with exit code 0.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,478 input=21,226 (+ 34,304 cached) output=252 (reasoning 105)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000001[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval untrusted
+[0m[1m[38;5;1merror:[0m invalid value '[0m[38;5;3muntrusted[0m' for '[0m[1m--ask-for-approval [0m'
+ [possible values: [0m[38;5;2mon-request[0m, [0m[38;5;2mnever[0m]
+
+For more information, try '[0m[1m--help[0m'.
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mI’m requesting approval to create that exact file outside the sandbox.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed; file existence was not independently verified.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,447 input=21,212 (+ 34,304 cached) output=235 (reasoning 67)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000002[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=none
+[0m[2m╭───────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0m[2m[3mloading[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0mloading[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────╯[0m
+
+[0m[48;2;65;69;76m [0m
+[0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m [0mError loading config.toml: unknown variant `none`, expected one of `user`, `auto_review`, `guardian_subagent`[0m[48;2;65;69;76m [0m
+in `approvals_reviewer`[0m[48;2;65;69;76m [0m
+ [0m[2m? for shortcuts[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=user
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mRequesting approval to run the exact command outside the sandbox.
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /Users/collie-user/codex-approval-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse the request_user_input tool ONCE: ask "Pick a fruit?" with options Apple and Pear, short descriptions. Do nothing else. [0m
+
+
+[0m[2m•[0m [0m[1mQuestions[0m[2m 1/1 answered[0m
+ • Pick a fruit?
+[0m[2m answer: [0m[38;5;6mPear[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Pear selected.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mCount slowly from 1 to 15, thinking carefully between each number. Do nothing else. [0m
+
+
+[0m[2m• [0m1
+
+ 2
+
+ 3
+
+ 4
+
+ 5
+
+ 6
+
+ 7
+
+ 8
+
+ 9
+
+ 10
+
+ 11
+
+ 12
+
+ 13
+
+ 14
+
+ 15
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /tmp/collie-codex-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[38;5;1m✗ [0mYou [0m[1mcanceled[0m the request to run [0m[2mtouch /tmp/collie-codex-probe.txt[0m
+
+[0m[1m[38;5;1m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /tmp/collie-codex-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[38;5;1m■ Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue.[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command again: touch /tmp/collie-codex-probe.txt (outside your sandbox — request approval). Do nothing else. [0m
+
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /tmp/collie-codex-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /tmp/collie-codex-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse request_user_input ONCE with TWO questions in one call: "Tabs or spaces?" options Tabs, Spaces; and "Semicolons?" options Yes, No. Short descriptions. Do [0m
+[0m[48;2;65;69;76m nothing else. [0m
+
+
+[0m[2m•[0m [0m[1mQuestions[0m[2m 2/2 answered[0m
+ • Tabs or spaces?
+[0m[2m answer: [0m[38;5;6mSpaces[0m
+ • Semicolons?
+[0m[2m answer: [0m[38;5;6mYes (Recommended)[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse request_user_input ONCE: "Throwaway?" options A, B. No descriptions. Do nothing else. [0m
+
+
+[0m[38;5;1m■ Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue.[0m
+Token usage: total=89,119 input=88,011 (+ 223,232 cached) output=1,108 (reasoning 503)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000003[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % cd /var/folders/r_/sanitizedtempdirtoken000000000/T/tmp.7lFep9Bv68 && codex --ask-for-approval on-request -c approv
+als_reviewer=user
+collie-user@collies-macbook-pro-1 tmp.7lFep9Bv68 % codex --ask-for-approval on-request -c approvals_reviewer=user
+
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.7lFep9Bv68[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m Try the [0m[1mDesktop app[0m. Run 'codex app' or visit https://chatgpt.com/codex?app-landing-page=true
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+[0m[48;2;65;69;76m [0m
+[0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m please summarize the architecture of this project in detail covering every module and its purpose and how they interact together and also explain the security [0m
+[0m[48;2;65;69;76m model plus the deployment story across each environment we support today [0m
+[0m[48;2;65;69;76m [0m
+ [0m[38;2;246;226;183mgpt-5.6-sol high[0m[2m · [0m[38;2;171;223;167m/private/var/folders/r_/sanitizedtempdirtoken000000000/T/tmp.7lFep9Bv68[0m[2m · [0m[38;2;242;181;144mContext 100% left[0m[2m · [0m[38;2;233;144;169mweekly 88% left[0m
\ No newline at end of file
diff --git a/web/src/fixtures/panes/codex--draft.txt b/web/src/fixtures/panes/codex--draft.txt
new file mode 100644
index 00000000..8895619c
--- /dev/null
+++ b/web/src/fixtures/panes/codex--draft.txt
@@ -0,0 +1,20 @@
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex
+
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m Run [0m[38;5;6mcodex app[0m to open the Desktop app (it installs on macOS if needed).
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+[0m[48;2;65;69;76m [0m
+[0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m hi there [0m
+[0m[48;2;65;69;76m [0m
+ [0m[38;2;246;226;183mgpt-5.6-sol high[0m[2m · [0m[38;2;171;223;167m/private/var/folders/r_/sanitizedtempdirtoken000000000/T/tmp.qHRsClGbNh[0m[2m · [0m[38;2;242;181;144mContext 100% left[0m[2m · [0m[38;2;233;144;169mweekly 88% left[0m
\ No newline at end of file
diff --git a/web/src/fixtures/panes/codex--fresh-idle.txt b/web/src/fixtures/panes/codex--fresh-idle.txt
new file mode 100644
index 00000000..7689b435
--- /dev/null
+++ b/web/src/fixtures/panes/codex--fresh-idle.txt
@@ -0,0 +1,20 @@
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex
+
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m Run [0m[38;5;6mcodex app[0m to open the Desktop app (it installs on macOS if needed).
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+[0m[48;2;65;69;76m [0m
+[0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m [0m[2m[48;2;65;69;76mAsk Codex to do anything[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+ [0m[38;2;246;226;183mgpt-5.6-sol high[0m[2m · [0m[38;2;171;223;167m/private/var/folders/r_/sanitizedtempdirtoken000000000/T/tmp.qHRsClGbNh[0m[2m · [0m[38;2;242;181;144mContext 100% left[0m[2m · [0m[38;2;233;144;169mweekly 88% left[0m
\ No newline at end of file
diff --git a/web/src/fixtures/panes/codex--trust-prompt.txt b/web/src/fixtures/panes/codex--trust-prompt.txt
new file mode 100644
index 00000000..e5966b08
--- /dev/null
+++ b/web/src/fixtures/panes/codex--trust-prompt.txt
@@ -0,0 +1,215 @@
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex
+
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m Run [0m[38;5;6mcodex app[0m to open the Desktop app (it installs on macOS if needed).
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (it is outside your sandbox, so it should need my approval). Do nothing else. [0m
+
+
+[0m[2m• [0mI’ll run exactly that command.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed with exit code 0.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,478 input=21,226 (+ 34,304 cached) output=252 (reasoning 105)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000001[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval untrusted
+[0m[1m[38;5;1merror:[0m invalid value '[0m[38;5;3muntrusted[0m' for '[0m[1m--ask-for-approval [0m'
+ [possible values: [0m[38;5;2mon-request[0m, [0m[38;5;2mnever[0m]
+
+For more information, try '[0m[1m--help[0m'.
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mI’m requesting approval to create that exact file outside the sandbox.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed; file existence was not independently verified.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,447 input=21,212 (+ 34,304 cached) output=235 (reasoning 67)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000002[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=none
+[0m[2m╭───────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0m[2m[3mloading[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0mloading[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────╯[0m
+
+[0m[48;2;65;69;76m [0m
+[0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m [0mError loading config.toml: unknown variant `none`, expected one of `user`, `auto_review`, `guardian_subagent`[0m[48;2;65;69;76m [0m
+in `approvals_reviewer`[0m[48;2;65;69;76m [0m
+ [0m[2m? for shortcuts[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=user
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mRequesting approval to run the exact command outside the sandbox.
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /Users/collie-user/codex-approval-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse the request_user_input tool ONCE: ask "Pick a fruit?" with options Apple and Pear, short descriptions. Do nothing else. [0m
+
+
+[0m[2m•[0m [0m[1mQuestions[0m[2m 1/1 answered[0m
+ • Pick a fruit?
+[0m[2m answer: [0m[38;5;6mPear[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Pear selected.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mCount slowly from 1 to 15, thinking carefully between each number. Do nothing else. [0m
+
+
+[0m[2m• [0m1
+
+ 2
+
+ 3
+
+ 4
+
+ 5
+
+ 6
+
+ 7
+
+ 8
+
+ 9
+
+ 10
+
+ 11
+
+ 12
+
+ 13
+
+ 14
+
+ 15
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /tmp/collie-codex-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[38;5;1m✗ [0mYou [0m[1mcanceled[0m the request to run [0m[2mtouch /tmp/collie-codex-probe.txt[0m
+
+[0m[1m[38;5;1m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /tmp/collie-codex-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[38;5;1m■ Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue.[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command again: touch /tmp/collie-codex-probe.txt (outside your sandbox — request approval). Do nothing else. [0m
+
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /tmp/collie-codex-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /tmp/collie-codex-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse request_user_input ONCE with TWO questions in one call: "Tabs or spaces?" options Tabs, Spaces; and "Semicolons?" options Yes, No. Short descriptions. Do [0m
+[0m[48;2;65;69;76m nothing else. [0m
+
+
+[0m[2m•[0m [0m[1mQuestions[0m[2m 2/2 answered[0m
+ • Tabs or spaces?
+[0m[2m answer: [0m[38;5;6mSpaces[0m
+ • Semicolons?
+[0m[2m answer: [0m[38;5;6mYes (Recommended)[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse request_user_input ONCE: "Throwaway?" options A, B. No descriptions. Do nothing else. [0m
+
+
+[0m[38;5;1m■ Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue.[0m
+Token usage: total=89,119 input=88,011 (+ 223,232 cached) output=1,108 (reasoning 503)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000003[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % cd /var/folders/r_/sanitizedtempdirtoken000000000/T/tmp.7lFep9Bv68 && codex --ask-for-approval on-request -c approv
+als_reviewer=user
+> [0m[1mYou are in [0m/private/var/folders/r_/sanitizedtempdirtoken000000000/T/tmp.7lFep9Bv68
+
+ Do you trust the contents of this directory? Working with untrusted contents comes with higher risk of prompt injection. Trusting the directory allows project-local
+ config, hooks, and exec policies to load.
+
+[0m[38;5;6m› 1. Yes, continue[0m
+ 2. No, quit
+
+ [0m[2mPress enter to continue[0m
\ No newline at end of file
diff --git a/web/src/fixtures/panes/codex--working.txt b/web/src/fixtures/panes/codex--working.txt
new file mode 100644
index 00000000..e62a3a14
--- /dev/null
+++ b/web/src/fixtures/panes/codex--working.txt
@@ -0,0 +1,140 @@
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex
+
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m Run [0m[38;5;6mcodex app[0m to open the Desktop app (it installs on macOS if needed).
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (it is outside your sandbox, so it should need my approval). Do nothing else. [0m
+
+
+[0m[2m• [0mI’ll run exactly that command.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed with exit code 0.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,478 input=21,226 (+ 34,304 cached) output=252 (reasoning 105)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000001[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval untrusted
+[0m[1m[38;5;1merror:[0m invalid value '[0m[38;5;3muntrusted[0m' for '[0m[1m--ask-for-approval [0m'
+ [possible values: [0m[38;5;2mon-request[0m, [0m[38;5;2mnever[0m]
+
+For more information, try '[0m[1m--help[0m'.
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mI’m requesting approval to create that exact file outside the sandbox.
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed; file existence was not independently verified.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+Token usage: total=21,447 input=21,212 (+ 34,304 cached) output=235 (reasoning 67)
+To continue this session, run [0m[38;5;6mcodex resume 00000000-0000-7000-8000-000000000002[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=none
+[0m[2m╭───────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0m[2m[3mloading[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0mloading[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────╯[0m
+
+[0m[48;2;65;69;76m [0m
+[0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m [0mError loading config.toml: unknown variant `none`, expected one of `user`, `auto_review`, `guardian_subagent`[0m[48;2;65;69;76m [0m
+in `approvals_reviewer`[0m[48;2;65;69;76m [0m
+ [0m[2m? for shortcuts[0m
+collie-user@collies-macbook-pro-1 tmp.qHRsClGbNh % codex --ask-for-approval on-request -c approvals_reviewer=user
+[0m[2m╭───────────────────────────────────────────────────────╮[0m
+[0m[2m│ >_ [0m[1mOpenAI Codex[0m[2m (v0.149.0) │[0m
+[0m[2m│ │[0m
+[0m[2m│ model: [0mgpt-5.6-sol high[0m[2m [0m[38;5;6m/model[0m[2m to change │[0m
+[0m[2m│ directory: [0m/private/var/folders/r_/…/T/tmp.qHRsClGbNh[0m[2m │[0m
+[0m[2m╰───────────────────────────────────────────────────────╯[0m
+
+ [0m[1mTip:[0m [0m[3mNew[0m Use [0m[1m/fast[0m to enable our fastest inference with increased plan usage.
+
+[0m[38;5;3m⚠ Under-development features enabled: default_mode_request_user_input. Under-development features are incomplete and may behave unpredictably. To suppress this[0m
+ [0m[38;5;3mwarning, set `suppress_unstable_features_warning = true` in /Users/collie-user/.codex/config.toml.[0m
+
+[0m[2m• [0mYou have 1 usage limit reset available. Run /usage to use one.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mRun this exact shell command: touch /Users/collie-user/codex-approval-probe.txt (outside your sandbox — request approval, do not work around it). Do nothing else. [0m
+
+
+[0m[2m• [0mRequesting approval to run the exact command outside the sandbox.
+
+[0m[38;5;2m✔ [0mYou [0m[1mapproved[0m codex to run [0m[2mtouch /Users/collie-user/codex-approval-probe.txt[0m[1m this time[0m
+
+[0m[1m[38;5;2m•[0m [0m[1mRan[0m [0m[38;2;137;180;250mtouch[0m[38;2;205;214;244m /Users/collie-user/codex-approval-probe.txt[0m
+[0m[2m └ (no output)[0m
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Command completed.
+
+[0m[2m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[0m
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mUse the request_user_input tool ONCE: ask "Pick a fruit?" with options Apple and Pear, short descriptions. Do nothing else. [0m
+
+
+[0m[2m•[0m [0m[1mQuestions[0m[2m 1/1 answered[0m
+ • Pick a fruit?
+[0m[2m answer: [0m[38;5;6mPear[0m
+
+[0m[2m• [0m[1m## TLDR:[0m
+
+ Pear selected.
+
+
+[0m[1m[2m[48;2;65;69;76m› [0m[48;2;65;69;76mCount slowly from 1 to 15, thinking carefully between each number. Do nothing else. [0m
+
+
+[0m[1m[38;2;255;255;255m•[0m [0m[1m[38;2;255;255;255mWorking[0m [0m[2m(3s • esc to interrupt)[0m
+
+[0m[48;2;65;69;76m [0m
+[0m[1m[48;2;65;69;76m›[0m[48;2;65;69;76m [0m[2m[48;2;65;69;76mAsk Codex to do anything[0m[48;2;65;69;76m [0m
+[0m[48;2;65;69;76m [0m
+ [0m[38;2;246;226;183mgpt-5.6-sol high[0m[2m · [0m[38;2;171;223;167m/private/var/folders/r_/sanitizedtempdirtoken000000000/T/tmp.qHRsClGbNh[0m[2m · [0m[38;2;242;181;144mContext 93% left[0m[2m · [0m[38;2;233;144;169mweekly 88% left[0m[2m · [0m[38;2;242;181;144m258K window[0m
\ No newline at end of file
diff --git a/web/src/lib/blocks.test.ts b/web/src/lib/blocks.test.ts
index f25df56d..37e21e2c 100644
--- a/web/src/lib/blocks.test.ts
+++ b/web/src/lib/blocks.test.ts
@@ -239,10 +239,11 @@ describe("buildBlocks — Claude grammars (ctx.agent === 'claude')", () => {
// nothing). What gates these grammars is the registry lookup, not the string "claude".
it("leaves an agent with no adapter as a single untouched raw block (conservative gating)", () => {
const lines = fixtureLines("claude--select-menu.txt");
- const blocks = buildBlocks(lines, { agent: "codex" });
+ // opencode: a real agent kind with NO adapter (codex graduated to one and no longer works here).
+ const blocks = buildBlocks(lines, { agent: "opencode" });
expect(blocks).toHaveLength(1);
expect(blocks[0]!.kind).toBe("raw");
- expect(blocks.some((b) => b.kind === "prompt-select")).toBe(false); // NO menu lifting for codex
+ expect(blocks.some((b) => b.kind === "prompt-select")).toBe(false); // NO menu lifting without an adapter
expect(blocks[0]!.lines).toBe(lines); // untouched — same reference
});
diff --git a/web/src/lib/harness/claude/index.ts b/web/src/lib/harness/claude/index.ts
index 23e6886b..996b1bb3 100644
--- a/web/src/lib/harness/claude/index.ts
+++ b/web/src/lib/harness/claude/index.ts
@@ -4,7 +4,7 @@
// wires them into the two HarnessAdapter surfaces: the block pipeline (claudeBuildBlocks) and the
// chrome re-surfacing probes (extractStatusLines / extractInputDraft, re-exported from ./chrome).
//
-// Every OTHER agent (codex, opencode, pi, a bare shell, or an unknown/absent agent) has an unverified
+// Every OTHER agent (opencode, pi, a bare shell, or an unknown/absent agent) has an unverified
// TUI shape, so it has no adapter and keeps the plain raw terminal mirror — running Claude's matchers
// on it could mis-lift a menu, strip real output as "chrome", or paint a bogus status strip.
diff --git a/web/src/lib/harness/codex.test.ts b/web/src/lib/harness/codex.test.ts
new file mode 100644
index 00000000..a840b779
--- /dev/null
+++ b/web/src/lib/harness/codex.test.ts
@@ -0,0 +1,320 @@
+import { readdirSync, readFileSync } from "node:fs";
+import { join } from "node:path";
+import { describe, expect, it } from "vitest";
+
+import { parseAnsi } from "../ansi";
+import { splitLines } from "../blocks";
+import { codexAdapter } from "./codex";
+import { locateComposer, stripChrome } from "./codex/chrome";
+import { lineText } from "./codex/markers";
+import { detectApprovalRegion } from "./codex/approval";
+import { detectAskRegion } from "./codex/ask";
+import { detectTrustRegion } from "./codex/trust";
+import { describeAdapterConformance } from "./conformance";
+
+const PANES_DIR = join(import.meta.dirname, "..", "..", "fixtures", "panes");
+
+const allCodexFixtures = readdirSync(PANES_DIR)
+ .filter((f) => f.startsWith("codex--") && f.endsWith(".txt"))
+ .sort();
+const allClaudeFixtures = readdirSync(PANES_DIR)
+ .filter((f) => f.startsWith("claude--") && f.endsWith(".txt"))
+ .sort();
+const allOmpFixtures = readdirSync(PANES_DIR)
+ .filter((f) => f.startsWith("omp--") && f.endsWith(".txt"))
+ .sort();
+
+const PINNED = [
+ "codex--approval-exec.txt",
+ "codex--ask-fruit.txt",
+ "codex--ask-notes-focused.txt",
+ "codex--ask-wizard-q1.txt",
+ "codex--ask-wizard-q2.txt",
+ "codex--draft-wrapped.txt",
+ "codex--draft.txt",
+ "codex--fresh-idle.txt",
+ "codex--trust-prompt.txt",
+ "codex--working.txt",
+];
+
+// The dialog captures — screens that lift an interactive block. The notes-focused ask is NOT
+// here: it is a live modal the adapter deliberately REFUSES (a digit would type into the notes
+// box), so it belongs to the neutral (raw-only) cohort with composerReady false.
+const DIALOG = [
+ "codex--approval-exec.txt",
+ "codex--ask-fruit.txt",
+ "codex--ask-wizard-q1.txt",
+ "codex--ask-wizard-q2.txt",
+ "codex--trust-prompt.txt",
+];
+
+const ownFixtures = DIALOG;
+const neutralFixtures = allCodexFixtures.filter((f) => !DIALOG.includes(f));
+
+describeAdapterConformance(codexAdapter, {
+ ownFixtures,
+ foreignFixtures: [...allClaudeFixtures, ...allOmpFixtures],
+ neutralFixtures,
+});
+
+describe("the codex corpus", () => {
+ it("is exactly the captures this adapter was developed against", () => {
+ expect(allCodexFixtures).toEqual(PINNED);
+ });
+});
+
+function fixtureLines(name: string) {
+ return splitLines(parseAnsi(readFileSync(join(PANES_DIR, name), "utf8")));
+}
+
+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"])(
+ "%s: the composer is on screen ⇒ true",
+ (name) => {
+ expect(codexAdapter.composerReady!(fixtureLines(name))).toBe(true);
+ },
+ );
+
+ it.each([...DIALOG, "codex--ask-notes-focused.txt"])("%s: a modal owns the screen ⇒ false", (name) => {
+ expect(codexAdapter.composerReady!(fixtureLines(name))).toBe(false);
+ });
+});
+
+describe("chrome", () => {
+ it("strips the prompt row and status row; the transcript stays", () => {
+ const lines = fixtureLines("codex--fresh-idle.txt");
+ const stripped = stripChrome(lines);
+ const text = stripped.map(lineText).join("\n");
+ expect(text).not.toContain("Ask Codex to do anything");
+ expect(text).not.toContain("Context 1");
+ expect(text).toContain("OpenAI Codex");
+ });
+
+ it("extracts a one-line draft, and null for the placeholder", () => {
+ expect(codexAdapter.extractInputDraft(fixtureLines("codex--draft.txt"))).toBe("hi there");
+ expect(codexAdapter.extractInputDraft(fixtureLines("codex--fresh-idle.txt"))).toBeNull();
+ });
+
+ it("joins a wrapped draft back into the typed sentence", () => {
+ expect(codexAdapter.extractInputDraft(fixtureLines("codex--draft-wrapped.txt"))).toBe(
+ "please summarize the architecture of this project in detail covering every module and its purpose and how they interact together and also explain the security model plus the deployment story across each environment we support today",
+ );
+ });
+
+ it("re-surfaces the status row and pairs composerPrompt with the ready screens", () => {
+ const lines = fixtureLines("codex--fresh-idle.txt");
+ const status = codexAdapter.extractStatusLines(lines);
+ expect(status).toHaveLength(1);
+ expect(lineText(status[0]!)).toMatch(/ · Context \d+% left/);
+ expect(codexAdapter.composerPrompt!(lines)).toMatch(/^› /);
+ });
+
+ it("a transcript `› ` echo without a status row beneath is not a composer", () => {
+ const screen = ["› some earlier submitted message", "• Working (3s • esc to interrupt)"].join("\n");
+ expect(locateComposer(splitLines(parseAnsi(screen)))).toBeNull();
+ expect(codexAdapter.composerReady!(splitLines(parseAnsi(screen)))).toBe(false);
+ });
+
+ it("a transcript echo above a status-LIKE prose row is not a composer (review repro)", () => {
+ // Column-0 prose mentioning a context percentage must not read as the status row…
+ const colZero = ["› a submitted transcript message", "", "model · Context 50% left"].join("\n");
+ expect(locateComposer(splitLines(parseAnsi(colZero)))).toBeNull();
+ // …nor indented prose with only ONE dot-separated field before the token.
+ const oneField = ["› a submitted transcript message", "", " model · Context 50% left"].join("\n");
+ expect(locateComposer(splitLines(parseAnsi(oneField)))).toBeNull();
+ // The real row shape (two fields before the token) still locates.
+ const real = ["› draft text", "", " model x · /some/dir · Context 50% left"].join("\n");
+ expect(locateComposer(splitLines(parseAnsi(real)))).not.toBeNull();
+ });
+
+ 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.
+ const cont = Array.from({ length: 8 }, (_, i) => ` word${i}`);
+ const lines = splitLines(
+ parseAnsi(["› start", ...cont, "", " model x · /some/dir · Context 50% left"].join("\n")),
+ );
+ expect(locateComposer(lines)).not.toBeNull();
+ expect(codexAdapter.composerReady!(lines)).toBe(true);
+ expect(codexAdapter.extractInputDraft(lines)).toBe(
+ ["start", ...Array.from({ length: 8 }, (_, i) => `word${i}`)].join(" "),
+ );
+ });
+
+ it("declines a draft taller than MAX_DRAFT_ROWS", () => {
+ const cont = Array.from({ length: 100 }, (_, i) => ` word${i}`);
+ const status = " model x · /some/dir · Context 50% left";
+ expect(
+ locateComposer(splitLines(parseAnsi(["› start", ...cont, "", status].join("\n")))),
+ ).toBeNull();
+ expect(
+ locateComposer(splitLines(parseAnsi(["› start", ...cont.slice(1), "", status].join("\n")))),
+ ).not.toBeNull();
+ });
+});
+
+describe("codexBuildBlocks", () => {
+ it("stays raw on every neutral capture", () => {
+ for (const name of neutralFixtures) {
+ const blocks = codexAdapter.buildBlocks(fixtureLines(name));
+ expect(blocks.every((b) => b.kind === "raw"), name).toBe(true);
+ }
+ });
+
+ it("lifts the trust prompt with digit keys — both probed on the captured widget", () => {
+ const prompt = codexAdapter.buildBlocks(fixtureLines("codex--trust-prompt.txt")).find(
+ (b) => b.kind === "prompt-select",
+ );
+ expect(prompt?.kind).toBe("prompt-select");
+ if (prompt?.kind !== "prompt-select") return;
+ expect(prompt.prompt.family).toBe("trust");
+ expect(prompt.prompt.options.map((o) => o.label)).toEqual(["Yes, continue", "No, quit"]);
+ expect(prompt.prompt.options.map((o) => o.keys)).toEqual([["1"], ["2"]]);
+ });
+
+ it("lifts the exec approval from its one-shot Yes / reject pair only", () => {
+ const lines = fixtureLines("codex--approval-exec.txt");
+ const blocks = codexAdapter.buildBlocks(lines);
+ const prompt = blocks.find((b) => b.kind === "prompt-select");
+ expect(prompt?.kind).toBe("prompt-select");
+ if (prompt?.kind !== "prompt-select") return;
+ expect(prompt.prompt.family).toBe("permission");
+ expect(prompt.prompt.options.map((o) => o.label)).toEqual([
+ "Yes, proceed",
+ "No, and tell Codex what to do differently",
+ ]);
+ expect(prompt.prompt.options.map((o) => o.keys)).toEqual([["1"], ["3"]]);
+ expect(prompt.prompt.options.some((o) => /don't ask again/i.test(o.label))).toBe(false);
+ // Header, Reason, `$ command`, and the persistent row stay in the raw mirror — swallowing
+ // the whole option run hid digit 2 from both the buttons and the phone.
+ const raw = blocks[0];
+ expect(raw?.kind).toBe("raw");
+ if (raw?.kind !== "raw") return;
+ const above = raw.lines.map(lineText).join("\n");
+ expect(above).toContain("Would you like to run the following command?");
+ expect(above).toContain("$ touch /tmp/collie-codex-probe.txt");
+ expect(above).toMatch(/2\.\s+Yes, and don't ask again/);
+ expect(prompt.lines.map(lineText).join("\n")).not.toMatch(/don't ask again/);
+ expect(lineText(prompt.lines[0]!)).toMatch(/3\.\s+No, and tell Codex/);
+ });
+
+ it("lifts a question card with per-row digits; the question stays in the mirror", () => {
+ const blocks = codexAdapter.buildBlocks(fixtureLines("codex--ask-fruit.txt"));
+ const prompt = blocks.find((b) => b.kind === "prompt-select");
+ expect(prompt?.kind).toBe("prompt-select");
+ if (prompt?.kind !== "prompt-select") return;
+ expect(prompt.prompt.family).toBe("select");
+ expect(prompt.prompt.question).toBe("Pick a fruit?");
+ expect(prompt.prompt.options.map((o) => o.label)).toEqual([
+ "Apple (Recommended)",
+ "Pear",
+ "None of the above",
+ ]);
+ expect(prompt.prompt.options.map((o) => o.keys)).toEqual([["1"], ["2"], ["3"]]);
+ expect(prompt.prompt.options[1]!.description).toBe("Choose a soft, juicy pear.");
+ const raw = blocks[0];
+ if (raw?.kind !== "raw") return;
+ expect(raw.lines.map(lineText).join("\n")).toContain("Pick a fruit?");
+ });
+
+ it("steps a multi-question set as consecutive lifted cards", () => {
+ for (const [name, question] of [
+ ["codex--ask-wizard-q1.txt", "Tabs or spaces?"],
+ ["codex--ask-wizard-q2.txt", "Semicolons?"],
+ ] as const) {
+ const prompt = codexAdapter.buildBlocks(fixtureLines(name)).find((b) => b.kind === "prompt-select");
+ expect(prompt?.kind, name).toBe("prompt-select");
+ if (prompt?.kind !== "prompt-select") return;
+ expect(prompt.prompt.question, name).toBe(question);
+ }
+ });
+
+ it("the notes-focused ask refuses to raw — a digit would type into the notes box", () => {
+ const lines = fixtureLines("codex--ask-notes-focused.txt");
+ expect(detectAskRegion(lines)).toBeNull();
+ expect(codexAdapter.buildBlocks(lines).every((b) => b.kind === "raw")).toBe(true);
+ });
+
+ it("approval refuses an unclassified middle row — no partial lift", () => {
+ const spoof = [
+ " Would you like to run the following command?",
+ " $ rm -rf /",
+ "› 1. Yes, proceed (y)",
+ " 2. Yes, just this directory",
+ " 3. No, and tell Codex what to do differently (esc)",
+ " Press enter to confirm or esc to cancel",
+ ].join("\n");
+ expect(detectApprovalRegion(splitLines(parseAnsi(spoof)))).toBeNull();
+ });
+
+ it("approval refuses a card whose last row is not the reject", () => {
+ const spoof = [
+ " Would you like to run the following command?",
+ " $ ls",
+ "› 1. Yes, proceed (y)",
+ " 2. Yes, and don't ask again for commands that start with `ls` (p)",
+ " 3. Yes, always",
+ " Press enter to confirm or esc to cancel",
+ ].join("\n");
+ expect(detectApprovalRegion(splitLines(parseAnsi(spoof)))).toBeNull();
+ });
+
+ it("approval refuses suffix-extended Yes/No labels — only the captured wording earns a key", () => {
+ const spoof = [
+ " Would you like to run the following command?",
+ " $ ls",
+ "› 1. Yes, proceed and remember forever (y)",
+ " 2. Yes, and don't ask again for commands that start with `ls` (p)",
+ " 3. No, and tell Codex what to do differently (esc)",
+ " Press enter to confirm or esc to cancel",
+ ].join("\n");
+ expect(detectApprovalRegion(splitLines(parseAnsi(spoof)))).toBeNull();
+ const spoofNo = [
+ " Would you like to run the following command?",
+ " $ ls",
+ "› 1. Yes, proceed (y)",
+ " 2. Yes, and don't ask again for commands that start with `ls` (p)",
+ " 3. No, and tell Codex what to do differently next time (esc)",
+ " Press enter to confirm or esc to cancel",
+ ].join("\n");
+ expect(detectApprovalRegion(splitLines(parseAnsi(spoofNo)))).toBeNull();
+ });
+
+ it("approval refuses when the header is missing — a bare option run is not the card", () => {
+ const spoof = [
+ "› 1. Yes, proceed (y)",
+ " 2. Yes, and don't ask again for commands that start with `ls` (p)",
+ " 3. No, and tell Codex what to do differently (esc)",
+ " Press enter to confirm or esc to cancel",
+ ].join("\n");
+ expect(detectApprovalRegion(splitLines(parseAnsi(spoof)))).toBeNull();
+ });
+
+ it("ask refuses non-consecutive digits and a missing header", () => {
+ const shuffled = [
+ " Question 1/1 (1 unanswered)",
+ " Pick?",
+ " › 2. B",
+ " 1. A",
+ " tab to add notes | enter to submit answer | esc to interrupt",
+ ].join("\n");
+ expect(detectAskRegion(splitLines(parseAnsi(shuffled)))).toBeNull();
+ const headerless = [
+ " Pick?",
+ " › 1. A",
+ " 2. B",
+ " tab to add notes | enter to submit answer | esc to interrupt",
+ ].join("\n");
+ expect(detectAskRegion(splitLines(parseAnsi(headerless)))).toBeNull();
+ });
+
+ it("trust refuses altered labels — a different pair of stakes is a different widget", () => {
+ const spoof = [
+ " Do you trust the contents of this directory? Working with untrusted contents…",
+ "› 1. Yes, always trust everything",
+ " 2. No, quit",
+ " Press enter to continue",
+ ].join("\n");
+ expect(detectTrustRegion(splitLines(parseAnsi(spoof)))).toBeNull();
+ });
+});
diff --git a/web/src/lib/harness/codex/APPROVAL_NOTES.md b/web/src/lib/harness/codex/APPROVAL_NOTES.md
new file mode 100644
index 00000000..d331fb4b
--- /dev/null
+++ b/web/src/lib/harness/codex/APPROVAL_NOTES.md
@@ -0,0 +1,43 @@
+# Codex exec-approval — keystroke recipe
+
+Captured 2026-08-22 on Codex v0.149.0 in a sandbox pane started with
+`--ask-for-approval on-request -c approvals_reviewer=user`. Note the config dependency: with
+`approvals_reviewer = "auto_review"` (this host's default) eligible approval requests are routed
+through a reviewer subagent instead of the user — the observed commands were approved with no
+dialog painted. `user` restores the human dialog. The card REPLACES the composer (no `› `
+prompt / status row at the tail). Herdr status: `blocked`.
+
+```
+ Would you like to run the following command?
+ Environment: local
+ Reason: Do you want to allow creating /tmp/collie-codex-probe.txt outside the sandbox?
+ $ touch /tmp/collie-codex-probe.txt
+› 1. Yes, proceed (y)
+ 2. Yes, and don't ask again for commands that start with `touch /tmp/collie-codex-probe.txt` (p)
+ 3. No, and tell Codex what to do differently (esc)
+ Press enter to confirm or esc to cancel
+```
+
+Live-probed, in this session:
+
+| Key | Effect |
+|---|---|
+| `y` | Confirms **Yes, proceed** immediately (file created). |
+| `1` | Same — digits confirm their row directly, no Enter (file created on a second card). |
+| `3` | Confirms **No** immediately; the command never ran (file verified absent ON DISK — negative control) and the conversation shows "interrupted — tell the model what to do differently". Rendering quirk: Codex still paints a `• Ran ` transcript row for the CANCELLED tool call — that row is the attempted call's entry, not execution evidence; the on-disk check is what proved non-execution. |
+| `Enter` | Confirms the **highlighted** row (default highlight was row 1). |
+
+Not probed (so not emitted): row 2 / `p` (persists an approval prefix — a mode change), bare
+`esc` (row 3's shortcut; digit 3 is the probed reject).
+
+What the adapter emits — the one-shot Yes and the reject only, and only when every row proves
+its class: the FIRST row must be `Yes, proceed`, the LAST row must be `No, and tell Codex what
+to do differently`, and every row between must match `don't ask again` (persistent — never a
+button). Any other row shape refuses the whole card. Digits are consecutive `1..n`; the footer
+and the `Would you like to run the following command?` header must both be on screen. The
+header, Reason, `$ command`, and persistent "don't ask again" rows stay in the raw mirror
+above the buttons, so the operator reads exactly what they are approving and still sees the
+persist option even though it is never a button.
+
+Only the exec-command approval is captured; other approval kinds (patch application, MCP
+consents, …) have different headers and fail closed until captured and probed.
diff --git a/web/src/lib/harness/codex/ASK_NOTES.md b/web/src/lib/harness/codex/ASK_NOTES.md
new file mode 100644
index 00000000..8ad58fa8
--- /dev/null
+++ b/web/src/lib/harness/codex/ASK_NOTES.md
@@ -0,0 +1,37 @@
+# Codex `request_user_input` — keystroke recipe
+
+Captured 2026-08-22 on Codex v0.149.0 in a sandbox pane (feature flag
+`default_mode_request_user_input` was enabled in the host config; the tool announces itself as
+under development). The card REPLACES the composer. Herdr status: `blocked`.
+
+```
+ Question 1/2 (2 unanswered)
+ Tabs or spaces?
+ › 1. Tabs (Recommended) Indent code with tab characters.
+ 2. Spaces Indent code with space characters.
+ 3. None of the above Optionally, add details in notes (tab).
+ tab to add notes | enter to submit answer | ←/→ to navigate questions | esc to interrupt
+```
+
+The final unanswered question's footer says `enter to submit all` instead. The tool auto-adds
+the `None of the above` row; label and description split on a 2+ space run, exactly like the
+option rows.
+
+Live-probed, in this session:
+
+| Key | Effect |
+|---|---|
+| digit `2` (single-question card) | Answered AND submitted immediately — despite the footer's `enter to submit answer` wording. |
+| digit `2` (question 1 of 2) | Answered question 1 and advanced to question 2. |
+| digit `1` (question 2 of 2) | Answered and submitted the WHOLE set (both answers registered). |
+| digit `3` (the auto-added `None of the above` row) | Answered AND submitted `"None of the above"` — the special row confirms like any other (probed through the send path on a two-option card). |
+| `tab` | Opens the notes box: a `› Add notes` row appears and the footer flips to `tab or esc to clear notes | enter to submit answer`. A second `tab` leaves it. |
+| `esc` | Interrupts the WHOLE conversation ("Conversation interrupted — tell the model what to do differently") — probed on a throwaway card. Never emitted. |
+
+What the adapter emits: one button per option row, `keys: ["N"]` — a digit answers the current
+question, which on the last unanswered question submits the set, so multi-question calls step
+through as consecutive lifted cards with no extra choreography. The complete captured layout is
+required: `Question X/Y (N unanswered)` header, a non-empty question line, consecutive `1..n`
+pointer rows, and the notes footer. The NOTES-FOCUSED state refuses to raw (footer
+`tab or esc to clear notes`, or a `› Add notes` row): a digit there would type into the box.
+Typing notes from the phone is deliberately not offered — it has no probed recipe.
diff --git a/web/src/lib/harness/codex/TRUST_NOTES.md b/web/src/lib/harness/codex/TRUST_NOTES.md
new file mode 100644
index 00000000..59e414f8
--- /dev/null
+++ b/web/src/lib/harness/codex/TRUST_NOTES.md
@@ -0,0 +1,27 @@
+# Codex folder-trust prompt — keystroke recipe
+
+Captured 2026-08-22 on Codex v0.149.0: the first screen when Codex starts in a directory it has
+not been told to trust. The card sits below the "Do you trust the contents of this directory?"
+paragraph; there is no composer or status row yet. Herdr status: `blocked` (herdr may also read
+it as idle briefly during startup).
+
+```
+> You are in /private/var/folders/…/T/tmp.7lFep9Bv68
+ Do you trust the contents of this directory? Working with untrusted contents comes with …
+ config, hooks, and exec policies to load.
+› 1. Yes, continue
+ 2. No, quit
+ Press enter to continue
+```
+
+Live-probed, in this session:
+
+| Key | Effect |
+|---|---|
+| `Enter` | Confirms the **highlighted** row (default: 1 — Codex continued into the session). |
+| `2` | Confirmed **No, quit** immediately — Codex exited to the shell. Digits confirm directly. |
+| `1` | Confirmed **Yes, continue** immediately — probed through the guarded send path on a second fresh directory; Codex continued into the session. |
+
+What the adapter emits: `Yes, continue` → `["1"]`, `No, quit` → `["2"]`, only on the exact
+captured layout (both labels, that order, the `Press enter to continue` tail row, and the trust
+question on screen above).
diff --git a/web/src/lib/harness/codex/approval.ts b/web/src/lib/harness/codex/approval.ts
new file mode 100644
index 00000000..d64de235
--- /dev/null
+++ b/web/src/lib/harness/codex/approval.ts
@@ -0,0 +1,98 @@
+// Codex's exec-approval dialog — "Would you like to run the following command?" over pointer-
+// numbered options with letter shortcuts, `Press enter to confirm or esc to cancel` as the tail
+// row. The card is CLASSIFIED, not layout-pinned (APPROVAL_NOTES.md): the first row must be the
+// one-shot Yes (`Yes, proceed (y)`), the last row the reject (`No, and tell Codex what to do
+// differently (esc)`), and every row between must PROVE it is a persistent mode change by its
+// label (`don't ask again …`) — those are never buttons. A row that fits no class refuses the
+// whole card. Digits confirm directly: `1` ran the approved command and `3` rejected it with
+// the command never running (both live-probed 2026-08-22, with the reject negative-controlled).
+// Pure; no pane access.
+
+import type { StyledLine } from "../../blocks";
+import type { PromptModel } from "../prompt-model";
+import { isBlank, lastNonBlankIndex, lineText, regionSignature, rstrip, skipBlanksUp } from "./markers";
+
+export interface ApprovalRegion {
+ model: PromptModel;
+ startLine: number;
+}
+
+const FOOTER = /^\s*Press enter to confirm or esc to cancel$/;
+const HEADER = /^\s*Would you like to run the following command\?$/;
+const OPTION = /^(?:› |\s{2})([1-9])\. (.+)$/;
+// EXACT labels (after the shortcut parenthetical is stripped), not prefixes: a suffix-extended
+// row ("Yes, proceed and remember forever") could carry persistent semantics behind a
+// one-shot-looking button (review repro). Only the captured wording earns a keystroke.
+const YES_ROW = /^Yes, proceed$/;
+const NO_ROW = /^No, and tell Codex what to do differently$/;
+const PERSISTENT = /don['’]t ask again/i;
+
+/** The button face: the row label minus its trailing keyboard-shortcut parenthetical. */
+function buttonLabel(label: string): string {
+ return label.replace(/\s*\([^)]*\)\s*$/, "").trim();
+}
+
+/** Exec-approval card at the tail, or null. */
+export function detectApprovalRegion(lines: StyledLine[]): ApprovalRegion | null {
+ const texts = lines.map((l) => rstrip(lineText(l)));
+ const fi = lastNonBlankIndex(texts);
+ if (fi < 0 || !FOOTER.test(texts[fi]!)) return null;
+
+ // One blank row separates the footer from the option run (every capture); the options
+ // themselves are contiguous. Visual order, not a Map: duplicates and shuffles must fail the
+ // digit sequence.
+ const bottom = skipBlanksUp(texts, fi - 1);
+ if (bottom < 0) return null;
+ const ordered: { digit: string; label: string }[] = [];
+ let i = bottom;
+ for (; i >= 0; i--) {
+ const opt = OPTION.exec(texts[i]!);
+ if (opt === null) break;
+ ordered.unshift({ digit: opt[1]!, label: opt[2]!.trim() });
+ }
+ const n = ordered.length;
+ if (n < 3) return null;
+ for (let k = 0; k < n; k++) {
+ if (ordered[k]!.digit !== String(k + 1)) return null;
+ }
+
+ const yes = ordered[0]!;
+ const no = ordered[n - 1]!;
+ if (!YES_ROW.test(buttonLabel(yes.label)) || !NO_ROW.test(buttonLabel(no.label))) return null;
+ for (let k = 1; k < n - 1; k++) {
+ if (!PERSISTENT.test(ordered[k]!.label)) return null;
+ }
+
+ // Between the options and the header sit the `$ command`, Reason and Environment rows —
+ // blank-separated content the mirror keeps. The header itself must be on screen within a
+ // short reach.
+ let headerRow = -1;
+ for (let k = i; k >= 0 && i - k < 12; k--) {
+ if (HEADER.test(texts[k]!)) {
+ headerRow = k;
+ break;
+ }
+ if (!isBlank(texts[k]!) && OPTION.test(texts[k]!)) return null;
+ }
+ if (headerRow < 0) return null;
+
+ const signature = regionSignature(lines, headerRow, fi + 1);
+ if (signature === "") return null;
+
+ return {
+ // Persistent rows sit between Yes and the reject. Replacing from the first option
+ // swallowed them (they are never buttons). The block starts at the reject so they
+ // stay in the raw mirror with the header, Reason, and `$ command`.
+ startLine: bottom,
+ model: {
+ question: "Would you like to run the following command?",
+ options: [
+ { label: buttonLabel(yes.label), keys: ["1"] },
+ { label: buttonLabel(no.label), keys: [String(n)] },
+ ],
+ family: "permission",
+ coreSignature: texts[headerRow]!.trim(),
+ signature,
+ },
+ };
+}
diff --git a/web/src/lib/harness/codex/ask.ts b/web/src/lib/harness/codex/ask.ts
new file mode 100644
index 00000000..995950b5
--- /dev/null
+++ b/web/src/lib/harness/codex/ask.ts
@@ -0,0 +1,91 @@
+// Codex's `request_user_input` question card — a `Question X/Y (N unanswered)` header, the
+// question line, pointer-numbered options with two-space-split descriptions (including the
+// tool's own auto-added "None of the above" row), and a `tab to add notes | enter to submit …`
+// footer. Digits confirm directly: a digit answers the CURRENT question, advancing a
+// multi-question set and submitting on the last one (live-probed 2026-08-22 on 1-question and
+// 2-question calls; ASK_NOTES.md). The notes flow stays in the terminal: when the notes box is
+// focused the footer flips to `tab or esc to clear notes …` and this detector refuses — a digit
+// would type into the box. Esc interrupts the WHOLE conversation and is never emitted. Pure;
+// no pane access.
+
+import type { StyledLine } from "../../blocks";
+import type { PromptModel, PromptOption } from "../prompt-model";
+import { lastNonBlankIndex, lineText, regionSignature, rstrip, skipBlanksUp } from "./markers";
+
+export interface AskRegion {
+ model: PromptModel;
+ startLine: number;
+}
+
+// Both captured footer variants start with the notes hint and carry an enter-submit verb
+// (`enter to submit answer` mid-set, `enter to submit all` on the final question).
+const FOOTER = /^\s*tab to add notes \| enter to submit\b/;
+// The notes-focused footer — the state in which a digit types instead of answering.
+const NOTES_FOOTER = /^\s*tab or esc to clear notes\b/;
+const NOTES_BOX = /^\s*› Add notes\b/;
+const HEADER = /^\s*Question (\d+)\/(\d+) \(\d+ unanswered\)$/;
+// Selected rows lead with ` › `, unselected with four spaces.
+const OPTION = /^(?:\s{2}› |\s{4})([1-9])\. (.+)$/;
+
+/** request_user_input card at the tail, or null. */
+export function detectAskRegion(lines: StyledLine[]): AskRegion | null {
+ const texts = lines.map((l) => rstrip(lineText(l)));
+ const fi = lastNonBlankIndex(texts);
+ if (fi < 0) return null;
+ // The notes-focused state is explicitly refused rather than merely unrecognized, so the
+ // refusal survives layout drift in the rows above.
+ if (NOTES_FOOTER.test(texts[fi]!)) return null;
+ if (!FOOTER.test(texts[fi]!)) return null;
+
+ // One blank row separates the footer from the option run; the options are contiguous.
+ const bottom = skipBlanksUp(texts, fi - 1);
+ if (bottom < 0) return null;
+ if (NOTES_BOX.test(texts[bottom]!)) return null;
+
+ const options: PromptOption[] = [];
+ let i = bottom;
+ for (; i >= 0; i--) {
+ const t = texts[i]!;
+ if (NOTES_BOX.test(t)) return null;
+ const opt = OPTION.exec(t);
+ if (opt === null) break;
+ const raw = opt[2]!.trim();
+ const split = raw.split(/\s{2,}/);
+ const label = (split[0] ?? raw).trim();
+ const description = split.slice(1).join(" ").trim();
+ const option: PromptOption = { label, keys: [opt[1]!] };
+ if (description !== "") option.description = description;
+ options.unshift(option);
+ }
+ if (options.length < 2) return null;
+ for (let k = 0; k < options.length; k++) {
+ if (options[k]!.keys[0] !== String(k + 1)) return null;
+ }
+
+ // Above the options (across one blank row): the question line, with the Question X/Y header
+ // directly above it. Both are required — they are what separates this card from any other
+ // pointer-numbered list.
+ const questionRow = skipBlanksUp(texts, i);
+ if (questionRow < 1) return null;
+ const question = texts[questionRow]!.trim();
+ if (question === "" || !HEADER.test(texts[questionRow - 1]!)) return null;
+
+ const start = bottom - options.length + 1;
+ const signature = regionSignature(lines, questionRow - 1, fi + 1);
+ if (signature === "") return null;
+
+ return {
+ // The block replaces the OPTIONS down; the header and question stay in the raw mirror.
+ startLine: start,
+ model: {
+ question,
+ options,
+ // `select` pins the renderer's caption; the KEYS carry this harness's probed recipe. The
+ // family doc describes Claude's digit-then-Enter — Codex's card submits on the digit alone
+ // (probed, ASK_NOTES.md), and the explicit per-option `keys` are what the send path uses.
+ family: "select",
+ coreSignature: question,
+ signature,
+ },
+ };
+}
diff --git a/web/src/lib/harness/codex/chrome.ts b/web/src/lib/harness/codex/chrome.ts
new file mode 100644
index 00000000..dd30d3ff
--- /dev/null
+++ b/web/src/lib/harness/codex/chrome.ts
@@ -0,0 +1,106 @@
+// Codex's chrome is boxless: a `› ` prompt row (wrapping onto two-space-indented continuation
+// rows) with the dot-separated status row directly beneath, sitting at the buffer tail. The
+// dialogs (trust / approval / ask) REPLACE that pair entirely — their own footer becomes the
+// tail — so locating the composer is also the composer-vs-modal discriminator. A submitted
+// message echoes into the transcript with the same `› ` prefix, which is why the walk anchors
+// on the STATUS row at the tail and only then looks up for the prompt row: an echo higher in
+// the transcript never has the status row directly beneath it. Pure; no pane access.
+
+import type { StyledLine } from "../../blocks";
+import {
+ isBlank,
+ isStatusRow,
+ lastNonBlankIndex,
+ lineText,
+ PLACEHOLDER,
+ promptText,
+ rstrip,
+ skipBlanksUp,
+} from "./markers";
+
+export interface ComposerBox {
+ /** The `› ` prompt row. */
+ promptRow: number;
+ /** The status row under it (last non-blank row of the frame). */
+ statusRow: number;
+}
+
+// A draft wraps onto indented continuation rows between the prompt row and the status row.
+// Captured drafts show one; the bound is slack for longer phone-typed messages. 8 stranded a
+// wrap (locateComposer returned null and the app reported a dialog). Same 100 as omp/Grok/
+// 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/;
+
+/** The composer at the buffer tail, or null (a dialog owns the screen, or the frame is torn). */
+export function locateComposer(lines: StyledLine[]): ComposerBox | null {
+ const texts = lines.map((l) => rstrip(lineText(l)));
+ const statusRow = lastNonBlankIndex(texts);
+ if (statusRow < 0 || !isStatusRow(texts[statusRow]!)) return null;
+
+ // One blank row separates the prompt/draft run from the status row (every capture); above the
+ // gap the run is CONTIGUOUS non-blank rows — wrapped-draft continuations under the `› ` prompt.
+ const top = skipBlanksUp(texts, statusRow - 1);
+ if (top < 0) return null;
+ for (let i = top; i >= 0 && top - i < MAX_DRAFT_ROWS; i--) {
+ const t = texts[i]!;
+ if (promptText(t) !== null) return { promptRow: i, statusRow };
+ // A blank or foreign-shaped row inside the run means this status row is not under a composer.
+ if (isBlank(t) || !CONTINUATION.test(t) || isStatusRow(t)) return null;
+ }
+ return null;
+}
+
+/**
+ * Return `lines` with the composer (prompt row through status row) removed from the tail.
+ * Unchanged input is the SAME REFERENCE, so callers can treat `result === lines` as "no chrome".
+ */
+export function stripChrome(lines: StyledLine[]): StyledLine[] {
+ const box = locateComposer(lines);
+ if (box === null) return lines;
+ return lines.slice(0, box.promptRow);
+}
+
+/** The status row, styled, for the strip above the phone composer. Empty when no composer. */
+export function extractStatusLines(lines: StyledLine[]): StyledLine[] {
+ const box = locateComposer(lines);
+ if (box === null) return [];
+ return [lines[box.statusRow]!];
+}
+
+/**
+ * The user's draft stranded in the composer: the `› ` row's text plus wrapped continuation
+ * rows, joined with single spaces (Codex word-wraps — verified against the typed original on
+ * the draft-wrapped capture). The placeholder is not a draft. Null = no composer / empty.
+ *
+ * Load-bearing: registering this adapter switches Codex panes from one-shot send to
+ * type-then-verify, and THIS is the verify half.
+ */
+export function extractInputDraft(lines: StyledLine[]): string | null {
+ const box = locateComposer(lines);
+ if (box === null) return null;
+ const texts = lines.map((l) => rstrip(lineText(l)));
+ const first = promptText(texts[box.promptRow]!) ?? "";
+ const parts = [first.trim()];
+ for (let i = box.promptRow + 1; i < box.statusRow; i++) {
+ parts.push(texts[i]!.trim());
+ }
+ const draft = parts.filter((p) => p !== "").join(" ");
+ if (draft === "" || draft === PLACEHOLDER) return null;
+ return draft;
+}
+
+/** Typing reaches the composer only when the composer is on screen — every dialog replaces it. */
+export function composerReady(lines: StyledLine[]): boolean {
+ return locateComposer(lines) !== null;
+}
+
+/** The literal on-screen prompt row a destructive pre-clear sweep is bound to. */
+export function composerPrompt(lines: StyledLine[]): string | null {
+ const box = locateComposer(lines);
+ if (box === null) return null;
+ return rstrip(lineText(lines[box.promptRow]!));
+}
diff --git a/web/src/lib/harness/codex/index.ts b/web/src/lib/harness/codex/index.ts
new file mode 100644
index 00000000..afec5947
--- /dev/null
+++ b/web/src/lib/harness/codex/index.ts
@@ -0,0 +1,72 @@
+// The Codex adapter. Chrome/status/draft are Tier 1: the boxless `› ` composer plus its
+// dot-separated status row are stripped from the mirror and re-surfaced natively. Interactive
+// kinds with dated captures and notes (all under this directory): the folder-trust prompt
+// (`prompt-select`, family trust), exec approvals (`prompt-select`, family permission —
+// classified by row: the one-shot Yes and the reject become buttons, persistent rows never do),
+// and `request_user_input` question cards (`prompt-select`, family select — a digit answers the
+// current question and submits on the last). Digits confirm directly on all three (probed;
+// notes files). The notes flow of a question card stays in the terminal: the focused-notes
+// state refuses to raw, because a digit would type into the box.
+//
+// The review bar is #99 (agy): exact agent string only, and every emitted keystroke probed on
+// the captured screen. Registered as `agent: "codex"`; variant folding belongs in
+// `canonicalAgent`, never here.
+
+import { trimTrailingBlank, type Block, type StyledLine } from "../../blocks";
+import type { HarnessAdapter } from "../types";
+import {
+ composerPrompt,
+ composerReady,
+ extractInputDraft,
+ extractStatusLines,
+ stripChrome,
+} from "./chrome";
+import { detectApprovalRegion } from "./approval";
+import { detectAskRegion } from "./ask";
+import { detectTrustRegion } from "./trust";
+
+export function codexBuildBlocks(lines: StyledLine[]): Block[] {
+ const trust = detectTrustRegion(lines);
+ if (trust) {
+ const before = trimTrailingBlank(lines.slice(0, trust.startLine));
+ const blocks: Block[] = [];
+ if (before.length > 0) blocks.push({ kind: "raw", lines: before });
+ blocks.push({ kind: "prompt-select", prompt: trust.model, lines: lines.slice(trust.startLine) });
+ return blocks;
+ }
+
+ const approval = detectApprovalRegion(lines);
+ if (approval) {
+ const before = trimTrailingBlank(lines.slice(0, approval.startLine));
+ const blocks: Block[] = [];
+ if (before.length > 0) blocks.push({ kind: "raw", lines: before });
+ blocks.push({
+ kind: "prompt-select",
+ prompt: approval.model,
+ lines: lines.slice(approval.startLine),
+ });
+ return blocks;
+ }
+
+ const ask = detectAskRegion(lines);
+ if (ask) {
+ const before = trimTrailingBlank(lines.slice(0, ask.startLine));
+ const blocks: Block[] = [];
+ if (before.length > 0) blocks.push({ kind: "raw", lines: before });
+ blocks.push({ kind: "prompt-select", prompt: ask.model, lines: lines.slice(ask.startLine) });
+ return blocks;
+ }
+
+ return [{ kind: "raw", lines: stripChrome(lines) }];
+}
+
+export { extractStatusLines, extractInputDraft };
+
+export const codexAdapter: HarnessAdapter = {
+ agent: "codex",
+ buildBlocks: codexBuildBlocks,
+ extractStatusLines,
+ extractInputDraft,
+ composerReady,
+ composerPrompt,
+};
diff --git a/web/src/lib/harness/codex/markers.ts b/web/src/lib/harness/codex/markers.ts
new file mode 100644
index 00000000..300af005
--- /dev/null
+++ b/web/src/lib/harness/codex/markers.ts
@@ -0,0 +1,86 @@
+// Shared lexing helpers over the parsed `StyledLine[]` — the primitives Codex's chrome stripping
+// and dialog grammars lean on. Same methodology as harness/claude/markers.ts and
+// harness/omp/markers.ts and deliberately NOT the same code: an adapter that imported another
+// adapter's predicates would inherit that harness's renderer archaeology. Codex's chrome is
+// BOXLESS: a bare `› ` prompt row wraps onto two-space-indented continuation rows, with a
+// dot-separated status row underneath — and a SUBMITTED message echoes into the transcript with
+// the same `› ` prefix, so nothing here is decisive without the tail anchoring locateComposer
+// does. They operate on the *parsed* line text (segment text joined), never raw ANSI bytes.
+// Pure functions, no I/O, no React.
+
+import { isBlank, lineText, type StyledLine } from "../../blocks";
+
+// `lineText` / `isBlank` are properties of a StyledLine, not of any grammar, so they live in the
+// neutral core (lib/blocks.ts). Re-exported here so the Codex grammars keep their single import
+// site — the same arrangement the other adapters use, for the same reason.
+export { isBlank, lineText };
+
+/** Drop TRAILING whitespace only. Codex pads rows to the pane width; an anchored `…$` regex
+ * would never match without this. Leading whitespace is load-bearing (it distinguishes the
+ * selected `› 1.` option row from the unselected ` 2.` one), so it stays. */
+export function rstrip(text: string): string {
+ return text.replace(/\s+$/, "");
+}
+
+// The status row under the composer: ` · · Context N% left[ · weekly N% left]`.
+// Everything before the Context token is OPAQUE — model names and directories change per
+// session and per release, and this file must never match them. What IS the grammar: the
+// two-space indent, at least two ` · `-separated fields before the token, and the token itself
+// (`left`, with `used` accepted — both spellings ship in the v0.149.0 binary). Requiring the
+// leading fields keeps a transcript line that merely mentions a context percentage from
+// claiming the row (review repro: `model · Context 50% left` at column 0 must not match).
+//
+// KNOWN LIMIT: Codex's status line is operator-configurable (`tui.status_line`, including
+// `null` to disable it). A custom or disabled status line never matches, so the composer is
+// never located and the pane falls back to the raw mirror with replies refused — safe, but
+// this adapter's lift only engages on the DEFAULT status line.
+const STATUS_ROW = /^ {2}\S.* · .* · .*Context \d+% (left|used)\b/;
+
+/** True when the row could be the composer's status line. Never decisive alone — the composer
+ * is located by the prompt-row-above-status shape at the buffer tail, not by any single row. */
+export function isStatusRow(text: string): boolean {
+ return STATUS_ROW.test(rstrip(text));
+}
+
+// The `› ` prompt row. Column 0 — but transcript ECHOES of submitted messages paint the same
+// prefix, so callers must only trust this at the located composer position.
+const PROMPT = /^› (.*)$/;
+
+/** Body of a `› ` prompt-shaped row (rstripped), or null when the line is not one. */
+export function promptText(text: string): string | null {
+ const m = PROMPT.exec(rstrip(text));
+ return m === null ? null : m[1]!;
+}
+
+/** The empty composer's placeholder, captured verbatim; a draft equal to it is no draft. */
+export const PLACEHOLDER = "Ask Codex to do anything";
+
+/** Index of the last non-blank row in `texts`, or -1 when the buffer is all blank. */
+export function lastNonBlankIndex(texts: string[]): number {
+ let i = texts.length - 1;
+ while (i >= 0 && isBlank(texts[i]!)) i--;
+ return i;
+}
+
+// Codex separates every section of a screen — prompt/status, options/footer, question/options —
+// with exactly one blank row (every 2026-08-22 capture). The gap helpers accept up to two so a
+// repaint wobble doesn't refuse a healthy frame; more means the rows aren't one widget.
+const MAX_SECTION_GAP = 2;
+
+/** The nearest non-blank row at or above `i`, or -1 when the blank gap exceeds the bound. */
+export function skipBlanksUp(texts: string[], i: number): number {
+ let gap = 0;
+ while (i >= 0 && isBlank(texts[i]!)) {
+ i--;
+ if (++gap > MAX_SECTION_GAP) return -1;
+ }
+ return i;
+}
+
+/** Join rstripped line text over `[from, to)` — the dialog signature the race guard compares. */
+export function regionSignature(lines: StyledLine[], from: number, to: number): string {
+ return lines
+ .slice(from, to)
+ .map((l) => rstrip(lineText(l)))
+ .join("\n");
+}
diff --git a/web/src/lib/harness/codex/trust.ts b/web/src/lib/harness/codex/trust.ts
new file mode 100644
index 00000000..f5b3817c
--- /dev/null
+++ b/web/src/lib/harness/codex/trust.ts
@@ -0,0 +1,69 @@
+// Codex's folder-trust prompt — the first screen in an untrusted directory. The captured layout
+// (TRUST_NOTES.md) is exactly two options with fixed labels under a "Do you trust the contents
+// of this directory?" paragraph, with `Press enter to continue` as the tail row. Digits confirm
+// directly: `2` quit Codex on the spot (live-probed 2026-08-22), and Enter confirms the
+// highlighted row. Anything off the captured layout refuses (fail-closed null). Pure; no pane
+// access.
+
+import type { StyledLine } from "../../blocks";
+import type { PromptModel } from "../prompt-model";
+import { lastNonBlankIndex, lineText, regionSignature, rstrip, skipBlanksUp } from "./markers";
+
+export interface TrustRegion {
+ model: PromptModel;
+ startLine: number;
+}
+
+const FOOTER = /^\s*Press enter to continue$/;
+// Selected rows lead with `› `, unselected with two spaces; both carry `N. label`.
+const OPTION = /^(?:› |\s{2})([12])\. (.+)$/;
+const YES_LABEL = /^Yes, continue$/;
+const NO_LABEL = /^No, quit$/;
+const QUESTION = /Do you trust the contents of this directory\?/;
+
+/** Trust prompt at the tail, or null. */
+export function detectTrustRegion(lines: StyledLine[]): TrustRegion | null {
+ const texts = lines.map((l) => rstrip(lineText(l)));
+ const fi = lastNonBlankIndex(texts);
+ if (fi < 2 || !FOOTER.test(texts[fi]!)) return null;
+
+ // One blank row separates the footer from the option pair; the options are contiguous.
+ const bottom = skipBlanksUp(texts, fi - 1);
+ if (bottom < 1) return null;
+ const two = OPTION.exec(texts[bottom]!);
+ const one = OPTION.exec(texts[bottom - 1]!);
+ if (one === null || two === null) return null;
+ if (one[1] !== "1" || two[1] !== "2") return null;
+ if (!YES_LABEL.test(one[2]!.trim()) || !NO_LABEL.test(two[2]!.trim())) return null;
+
+ // The question paragraph sits in the rows above the options (across one blank row); require
+ // it on screen so an out-of-context pair of rows can't claim the recipe.
+ let questionRow = -1;
+ for (let i = bottom - 2; i >= 0 && bottom - 2 - i < 6; i--) {
+ if (QUESTION.test(texts[i]!)) {
+ questionRow = i;
+ break;
+ }
+ }
+ if (questionRow < 0) return null;
+
+ const start = bottom - 1;
+ // The signature runs from the question paragraph through the footer — the subject above the
+ // options participates, so the race guard sees a screen whose context changed under the user.
+ const signature = regionSignature(lines, questionRow, fi + 1);
+ if (signature === "") return null;
+
+ return {
+ startLine: start,
+ model: {
+ question: "Do you trust the contents of this directory?",
+ options: [
+ { label: "Yes, continue", keys: ["1"] },
+ { label: "No, quit", keys: ["2"] },
+ ],
+ family: "trust",
+ coreSignature: "Do you trust the contents of this directory?",
+ signature,
+ },
+ };
+}
diff --git a/web/src/lib/harness/conformance.test.ts b/web/src/lib/harness/conformance.test.ts
index ad04598f..1fe44844 100644
--- a/web/src/lib/harness/conformance.test.ts
+++ b/web/src/lib/harness/conformance.test.ts
@@ -13,10 +13,10 @@ import { describeAdapterConformance, isValidHerdrKey } from "./conformance";
// captures whose block may still be wiring up (a not-yet-detecting own fixture is tolerated by the
// suite, see conformance.ts).
//
-// The FOREIGN cohort is the omp corpus — the second adapter's captures. Until it existed this leg was
-// vacuous (`foreignFixtures: []`), so "an adapter must stay raw on another harness's screens" was a
-// documented promise nothing checked. Claude's grammars return raw-only on all 20 omp captures today,
-// and the mirror-image assertion lives in harness/omp.test.ts.
+// The FOREIGN cohort is the other adapters' corpora (omp + codex). Until a second corpus existed this
+// leg was vacuous (`foreignFixtures: []`), so "an adapter must stay raw on another harness's screens"
+// was a documented promise nothing checked. The mirror-image assertions live in harness/omp.test.ts
+// and harness/codex.test.ts.
const PANES_DIR = join(import.meta.dirname, "..", "..", "fixtures", "panes");
@@ -55,13 +55,16 @@ const allClaudeFixtures = readdirSync(PANES_DIR)
const allOmpFixtures = readdirSync(PANES_DIR)
.filter((f) => f.startsWith("omp--") && f.endsWith(".txt"))
.sort();
+const allCodexFixtures = readdirSync(PANES_DIR)
+ .filter((f) => f.startsWith("codex--") && f.endsWith(".txt"))
+ .sort();
const ownFixtures = allClaudeFixtures.filter((f) => !NEUTRAL.includes(f));
const neutralFixtures = allClaudeFixtures.filter((f) => NEUTRAL.includes(f));
describeAdapterConformance(claudeAdapter, {
ownFixtures,
- foreignFixtures: allOmpFixtures, // the second adapter's captures — cross-adapter fail-closed
+ foreignFixtures: [...allOmpFixtures, ...allCodexFixtures], // the other adapters' captures — cross-adapter fail-closed
neutralFixtures,
});
diff --git a/web/src/lib/harness/omp.test.ts b/web/src/lib/harness/omp.test.ts
index 22197276..a7f4fae3 100644
--- a/web/src/lib/harness/omp.test.ts
+++ b/web/src/lib/harness/omp.test.ts
@@ -15,9 +15,9 @@ import { parseKeyHintFooter } from "./menu-hints";
// entire corpus rather than over a chosen subset: no interactive block kind is ever constructed, so no
// tap can reach a keystroke. See harness/omp/index.ts for why the dialog layer is a later PR.
//
-// This is also the first time the FOREIGN cohort is non-empty for anybody: `foreignFixtures` is every
-// claude capture, which pins the cross-adapter fail-closed leg that had never been exercised before.
-// The other half of that loop lives in conformance.test.ts, where Claude's leg now takes omp--*.
+// The FOREIGN cohort is every claude and codex capture, which pins the cross-adapter fail-closed leg.
+// The other directions of that loop live in conformance.test.ts (Claude's leg takes omp--* + codex--*)
+// and harness/codex.test.ts (codex's leg takes claude--* + omp--*).
const PANES_DIR = join(import.meta.dirname, "..", "..", "fixtures", "panes");
@@ -27,6 +27,9 @@ const allOmpFixtures = readdirSync(PANES_DIR)
const allClaudeFixtures = readdirSync(PANES_DIR)
.filter((f) => f.startsWith("claude--") && f.endsWith(".txt"))
.sort();
+const allCodexFixtures = readdirSync(PANES_DIR)
+ .filter((f) => f.startsWith("codex--") && f.endsWith(".txt"))
+ .sort();
// Every omp screen this adapter DECLINES — which is every screen IN THIS CORPUS, not every screen omp
// can draw (omp's tool-approval dialog, in particular, was never captured; see omp/index.ts). These
@@ -80,7 +83,7 @@ const neutralFixtures = allOmpFixtures.filter((f) => DECLINED.includes(f));
describeAdapterConformance(ompAdapter, {
ownFixtures,
- foreignFixtures: allClaudeFixtures,
+ foreignFixtures: [...allClaudeFixtures, ...allCodexFixtures],
neutralFixtures,
});
diff --git a/web/src/lib/harness/registry.test.ts b/web/src/lib/harness/registry.test.ts
index 4a9d2da8..297d4acd 100644
--- a/web/src/lib/harness/registry.test.ts
+++ b/web/src/lib/harness/registry.test.ts
@@ -12,11 +12,13 @@ describe("hasBlockGrammar", () => {
// and does not claim to. What this predicate actually answers is "does an adapter exist".
it("is true for every registered adapter", () => {
expect(hasBlockGrammar("claude")).toBe(true);
+ expect(hasBlockGrammar("codex")).toBe(true);
expect(hasBlockGrammar("omp")).toBe(true);
});
it("is false for every unregistered agent (no adapter ⇒ raw mirror)", () => {
- for (const agent of ["codex", "opencode", "pi", "shell", "unknown"]) {
+ // Exact strings only: the codex ADAPTER must not leak to variant spellings (#99).
+ for (const agent of ["opencode", "pi", "shell", "unknown", "Codex", "codex-cli"]) {
expect(hasBlockGrammar(agent)).toBe(false);
}
});
diff --git a/web/src/lib/harness/registry.ts b/web/src/lib/harness/registry.ts
index 3da792a0..d5dc8eb2 100644
--- a/web/src/lib/harness/registry.ts
+++ b/web/src/lib/harness/registry.ts
@@ -2,20 +2,25 @@
// Herdr snapshot `agent` string to its HarnessAdapter; anything absent from the map has no adapter,
// so it keeps the universal raw mirror (the T1 fallback). Both gates route through here — the render
// pipeline (harness/index buildBlocks) and agent-chat's status strip — so the policy can't drift, and
-// adding a further verified agent is a one-line change to ADAPTERS. The list holds two today: claude,
-// which lifts every block kind, and omp, which is Tier 1 and lifts none — it contributes chrome
-// stripping and the composer gate only. `hasBlockGrammar` replaces the old grammar/agents predicate:
+// adding a further verified agent is a one-line change to ADAPTERS. The list holds three today:
+// claude, which lifts every block kind; codex, which is Tier 1 chrome plus Tier-2 probed trust /
+// approval / question lifts; and omp, which is Tier 1 and lifts none — it contributes chrome
+// stripping and the composer gate only. Adapters register by their EXACT agent string only —
+// prefix-matching here was the AltanS/collie#99 reject: it would hand a harness's live keystroke
+// recipes to any agent string sharing the prefix. `hasBlockGrammar` replaces the old
+// grammar/agents predicate:
// it's now just "is there an adapter for this agent?", which is the question both gates actually ask
// (a Tier-1 adapter still owns its pane's pipeline; what it BUILDS is the adapter's own business).
import type { HarnessAdapter } from "./types";
import { claudeAdapter } from "./claude";
+import { codexAdapter } from "./codex";
import { ompAdapter } from "./omp";
// Built FROM the adapter list (not a hand-written literal) so a key can't silently drift from its
// adapter's own `agent` string — the map key IS `adapter.agent`.
const ADAPTERS: Record = Object.fromEntries(
- [claudeAdapter, ompAdapter].map((a) => [a.agent, a]),
+ [claudeAdapter, codexAdapter, ompAdapter].map((a) => [a.agent, a]),
);
/** The adapter for `agent`, or undefined when the agent is unknown/absent (→ raw fallback). `Object.hasOwn`
diff --git a/web/src/lib/harness/types.ts b/web/src/lib/harness/types.ts
index 12e0a6fc..0dd44847 100644
--- a/web/src/lib/harness/types.ts
+++ b/web/src/lib/harness/types.ts
@@ -1,4 +1,4 @@
-// The pluggable detection seam. Each supported agent (Claude and omp today; codex/opencode/… tomorrow)
+// The pluggable detection seam. Each supported agent (claude, codex and omp today; opencode/pi/… tomorrow)
// contributes ONE HarnessAdapter: its own block-building pipeline plus the two chrome re-surfacing
// probes (the statusline the mirror strips, and a stranded input-box draft). The registry
// (registry.ts) maps a Herdr snapshot `agent` string to its adapter; everything not in the registry
diff --git a/web/src/lib/quick-replies.ts b/web/src/lib/quick-replies.ts
index be3790f3..519269b5 100644
--- a/web/src/lib/quick-replies.ts
+++ b/web/src/lib/quick-replies.ts
@@ -4,7 +4,7 @@
// read through a tolerant lookup) and deliberately NOT on the harness adapter registry: an adapter
// is DETECTION only — grammars for lifting dialogs out of a terminal buffer — and a list of English
// phrases is content policy, not detection. Bolting these onto HarnessAdapter would also strand
-// codex/pi/opencode, which have no adapter at all (they run the raw-mirror fallback) yet still want
+// pi/opencode, which have no adapter at all (they run the raw-mirror fallback) yet still want
// quick replies. Keeping them here means a per-agent divergence later is a data edit, not a refactor.
//
// The split that actually matters TODAY is agent vs shell, not agent vs agent: "yes"/"continue" mean