From 62fe58e36bd66e78b605a205c6011b0f4a17e7b5 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 10 Oct 2026 16:39:53 -0700 Subject: [PATCH 1/5] Delete the session-format v1 migration; v1 files load verbatim --- docs/sessions.md | 16 +--- src/session.rs | 230 +++++++---------------------------------------- 2 files changed, 33 insertions(+), 213 deletions(-) diff --git a/docs/sessions.md b/docs/sessions.md index da303b1..88c9d14 100644 --- a/docs/sessions.md +++ b/docs/sessions.md @@ -46,7 +46,7 @@ A session file is a JSON object with three fields: `version` for the format vers - Values are ordered lists, loaded in order. Each task is started in its own PTY under the specified directory. - Directories are serialized alphabetically, with entry order preserved within each directory. -The `version` field must be an integer from 1 through the newest format supported by the running `fleetcom`. A missing field means version 1. Invalid or unsupported versions are refused on load, with the file's value and supported version in the error. A version 2 file read by an older `fleetcom` is refused by that same gate. +The `version` field must be an integer from 1 through the newest format supported by the running `fleetcom`. A missing field means version 1. A version 1 file loads with every entry literal, including `claude` and `claude --resume ''`; saving writes those back as literals, so to track such a task's conversation, launch it from the Agent page and save again. Invalid or unsupported versions are refused on load, with the file's value and supported version in the error. A version 2 file read by an older `fleetcom` is refused by that same gate. The schema is identified by shape, independently of the version. With an object-valued `dirs`, the wrapped form above is used. A flat map is also accepted, with directories as top-level keys and entry arrays as values. In that form, an array-valued key named `dirs` remains a directory entry, but a top-level `version` member is always the format version, never a directory. Flat-map files are listed by filename stem because no name is stored. On save, the wrapped form is written, without a stored-name collision check. @@ -60,20 +60,6 @@ After validation, group and display names are stripped of control characters and Use the string form for commands with neither a group nor a name. String and object entries can appear in the same directory array. -### Loading version 1 files - -In format 1, instrumented tasks were stored as command strings: a bare program word, or the word followed by its resume selector and a single-quoted UUID. When loading a file with `version` 1 or no version field, convert exactly those two forms to managed entries: - -| Version 1 entry | Loads as | -| -- | -- | -| `claude`, `codex`, `grok`, or `omp` alone | managed, fresh conversation | -| `claude --resume ''`, `codex resume ''`, `grok --resume ''`, or `omp --resume ''`, with or without the quotes | managed, resuming that ID | -| anything else | literal, unchanged | - -Tabs and repeated spaces between tokens are still accepted. Commands with paths (`/usr/local/bin/claude`, `~/bin/claude`) are loaded as literals to preserve your chosen binary instead of substituting one found on `PATH`; managed resume tracking is then unavailable. Commands with extra arguments, prompts, other resume spellings, or shell syntax remain literal, as before. No conversion is applied to version 2 files: a literal `claude` stays literal. - -Recovery snapshots are rewritten continually in the new format; named sessions are converted on the next save. This conversion will be removed one release after format 2 ships. After that, all entries in unconverted version 1 files will be loaded as literals. - ## Recovery The current task set is automatically snapshotted under `recovery/` inside the session directory. A separate file is written for each daemon (or `--foreground` core), named for its start time and process ID. After each write, the new file and files whose process IDs are still live are protected; among the remaining files, the nine newest names are retained. A shared recovery directory can therefore contain more than ten snapshots while multiple writers are live. diff --git a/src/session.rs b/src/session.rs index bf6298b..0208600 100644 --- a/src/session.rs +++ b/src/session.rs @@ -108,8 +108,12 @@ pub fn sessions_dir(root: Option) -> Option { } /// Session format version written by `to_json` and accepted by `from_json`. Managed entries -/// were introduced in version 2. A missing version means 1; `from_json` loads a v1 file as -/// literals, then rewrites the recipe through [`migrate_v1`]. Reject newer versions. +/// were introduced in version 2. A missing version means 1; `from_json` loads a v1 file +/// verbatim, every entry literal. Through 0.15.0 the loader rewrote a v1 file's bare agent +/// words and canonical resume commands into managed entries, because v1 had no managed form; +/// the release after 0.15.0 retired that rewrite, so an unconverted v1 file loads without +/// managed resume tracking, and `to_json` writes its entries back as literals. Reject newer +/// versions. const FORMAT_VERSION: u64 = 2; /// Build the recipe's `dirs` object. @@ -140,52 +144,6 @@ fn dirs_json(cfg: &SessionConfig) -> jzon::JsonValue { dirs } -/// Rewrite a v1 recipe's literal entries in place to the corresponding managed form; labels -/// are untouched. In v1, bare program words and canonical resume commands were -/// instrumented; preserving them as literals would silently disable conversation tracking. -/// Convert exactly those two forms, accepting v1's whitespace between tokens and -/// single-quoted IDs. Require an exact registered word as the first token. With a path -/// (`/usr/local/bin/claude`, `~/bin/claude`), preserve the user's chosen binary as a literal -/// rather than substitute the first binary found on `PATH`; managed resume tracking is then -/// unavailable. Leave all other commands literal. -/// -/// Phase 5, one release after format v2 ships, deletes this function, its one call in -/// `from_json`, and its tests (`v1_literals_migrate_exactly_the_two_instrumented_shapes`, -/// `v1_recovery_snapshots_migrate_on_load`). A v1 file still loads afterward, with every -/// entry literal: the file is accepted, managed resume tracking is not. -fn migrate_v1(cfg: &mut SessionConfig) { - /// The managed form of one v1 literal, or `None` to leave it literal. - fn managed_form(cmd: &str) -> Option { - let mut words = cmd.split([' ', '\t']).filter(|w| !w.is_empty()); - let (agent, selector) = words.next().and_then(harness::registered)?.shape(); - let resume = match (words.next(), words.next(), words.next()) { - (None, _, _) => None, - (Some(sel), Some(id), None) if sel == selector => { - let id = id - .strip_prefix('\'') - .and_then(|t| t.strip_suffix('\'')) - .unwrap_or(id); - if !harness::is_uuid(id) { - return None; - } - Some(id.to_string()) - } - _ => return None, - }; - Some(EntryKind::Managed { - agent: agent.to_string(), - resume, - }) - } - for entry in cfg.values_mut().flatten() { - if let EntryKind::Literal(cmd) = &entry.kind - && let Some(kind) = managed_form(cmd) - { - entry.kind = kind; - } - } -} - /// Serialize the versioned wrapped schema. Store the name to distinguish names /// sanitized to the same filename. fn to_json(name: &str, cfg: &SessionConfig) -> String { @@ -208,17 +166,20 @@ pub fn fingerprint_json(cfg: &SessionConfig) -> String { /// through [`FORMAT_VERSION`]. Treat a missing version as 1. Accept entry forms regardless /// of version: strings and `{"cmd"}` objects for literals, `{"agent"}` objects for managed /// agents. Reject the whole file for objects with both keys, unregistered agents, or resume -/// IDs that are not strict UUIDs. Pass a v1 recipe through [`migrate_v1`] once it is built. +/// IDs that are not strict UUIDs. Never rewrite an entry: a v1 file loads verbatim, every +/// entry literal. The rewrite that converted v1 agent commands to managed entries (v1 had no +/// managed form) shipped in 0.15.0 and was retired in the release after it. fn from_json(text: &str) -> io::Result<(Option, SessionConfig)> { let invalid = |message| io::Error::new(io::ErrorKind::InvalidData, message); let parsed = jzon::parse(text).map_err(|e| invalid(e.to_string()))?; if !parsed.is_object() { return Err(invalid("session root: expected an object".into())); } - // Validate version metadata before detecting the schema shape. - let version = if parsed.has_key("version") { + // Validate version metadata before detecting the schema shape. The value gates + // nothing else: every accepted version shares one entry grammar. + if parsed.has_key("version") { match parsed["version"].as_u64() { - Some(n) if (1..=FORMAT_VERSION).contains(&n) => n, + Some(n) if (1..=FORMAT_VERSION).contains(&n) => {} Some(n) if n > FORMAT_VERSION => { return Err(invalid(format!( "session format version {n} is newer than this fleetcom \ @@ -234,9 +195,7 @@ fn from_json(text: &str) -> io::Result<(Option, SessionConfig)> { ))); } } - } else { - 1 - }; + } let (name, dirs, flat) = if parsed["dirs"].is_object() { let name = opt_str(&parsed["name"]) .ok_or_else(|| invalid("session field \"name\": expected a string or null".into()))?; @@ -317,10 +276,6 @@ fn from_json(text: &str) -> io::Result<(Option, SessionConfig)> { } cfg.insert(dir.to_string(), entries); } - // In v1, managed launches could only be stored as command text. - if version == 1 { - migrate_v1(&mut cfg); - } Ok((name, cfg)) } @@ -793,120 +748,27 @@ mod tests { assert_eq!(fp(Some(ID)), fp(Some(ID))); } - /// Preserve literal agent words in v2 files; migrate only v1 literals. - #[test] - fn v2_literal_agent_words_stay_literal() { - let (_, cfg) = from_json(&format!( - r#"{{"version": 2, "name": "v", "dirs": {{"d": ["claude", - {{"cmd": "codex resume '{ID}'", "group": "g"}}, {{"agent": "omp"}}]}}}}"# - )) - .unwrap(); - assert_eq!( - cfg["d"], - vec![ - e("claude"), - ge(&format!("codex resume '{ID}'"), "g"), - m("omp", None) - ] - ); - } - - /// Convert the two instrumented v1 forms to managed entries, accepting the original - /// whitespace and quoted IDs. Keep paths, extra arguments, shell syntax, and malformed - /// IDs literal. + /// Preserve literal agent words and resume commands in every accepted version. A v1 + /// file now loads verbatim: the rewrite to managed entries is gone, so the spellings + /// it once converted stay literal beside a v2 managed entry. #[test] - fn v1_literals_migrate_exactly_the_two_instrumented_shapes() { - let cases: Vec<(String, SessionEntry)> = vec![ - ("claude".into(), m("claude", None)), - ("codex".into(), m("codex", None)), - ("grok".into(), m("grok", None)), - ("omp".into(), m("omp", None)), - (format!("claude --resume {ID}"), m("claude", Some(ID))), - (format!("claude --resume '{ID}'"), m("claude", Some(ID))), - (format!("codex resume '{ID}'"), m("codex", Some(ID))), - (format!("grok --resume {ID}"), m("grok", Some(ID))), - (format!("omp --resume '{ID}'"), m("omp", Some(ID))), - // v1's token tolerance: tabs, repeated and surrounding whitespace. - (" claude\t".into(), m("claude", None)), - (format!("claude\t--resume '{ID}' "), m("claude", Some(ID))), - // Preserve the user's chosen binary path as a literal, without managed resume - // tracking. - ("/usr/local/bin/claude".into(), e("/usr/local/bin/claude")), - ("~/bin/claude".into(), e("~/bin/claude")), - ( - format!("/usr/local/bin/claude --resume '{ID}'"), - e(&format!("/usr/local/bin/claude --resume '{ID}'")), - ), - // Arguments, prompts, the wrong selector, and shell syntax. - ("claude --model opus".into(), e("claude --model opus")), - ("claude 'fix the tests'".into(), e("claude 'fix the tests'")), - (format!("claude -r {ID}"), e(&format!("claude -r {ID}"))), - ( - format!("codex --resume {ID}"), - e(&format!("codex --resume {ID}")), - ), - ( - format!("claude --resume {ID} --model opus"), - e(&format!("claude --resume {ID} --model opus")), - ), - ("claude --resume".into(), e("claude --resume")), - ( - "claude --resume not-a-uuid".into(), - e("claude --resume not-a-uuid"), - ), - ( - format!("claude --resume '{ID}"), - e(&format!("claude --resume '{ID}")), - ), - ( - format!("claude --resume ''{ID}''"), - e(&format!("claude --resume ''{ID}''")), - ), - ("claude --resume $ID".into(), e("claude --resume $ID")), - ("claude | tee log".into(), e("claude | tee log")), - ("claude; ls".into(), e("claude; ls")), - ("FOO=bar claude".into(), e("FOO=bar claude")), - ("'claude'".into(), e("'claude'")), - ("claudius".into(), e("claudius")), - ("vim".into(), e("vim")), - (String::new(), e("")), - ]; - let mut cfg = SessionConfig::from([( - "d".to_string(), - cases.iter().map(|(cmd, _)| e(cmd)).collect(), - )]); - migrate_v1(&mut cfg); - for ((cmd, expected), got) in cases.iter().zip(&cfg["d"]) { - assert_eq!(got, expected, "{cmd:?}"); - } - // Through the loader: every v1 spelling, wrapped and flat, string and - // object form, with labels carried over. - let member = jzon::JsonValue::from(cases[4].0.as_str()).dump(); - for text in [ - format!(r#"{{"version": 1, "name": "v", "dirs": {{"d": [{member}]}}}}"#), - format!(r#"{{"name": "v", "dirs": {{"d": [{member}]}}}}"#), - format!(r#"{{"d": [{member}]}}"#), - format!(r#"{{"version": 1, "d": [{member}]}}"#), - ] { + fn literal_agent_words_stay_literal_in_every_version() { + for version in ["", r#""version": 1, "#, r#""version": 2, "#] { + let text = format!( + r#"{{{version}"name": "v", "dirs": {{"d": ["claude", + {{"cmd": "codex resume '{ID}'", "group": "g"}}, {{"agent": "omp"}}]}}}}"# + ); let (_, cfg) = from_json(&text).unwrap(); - assert_eq!(cfg["d"], vec![m("claude", Some(ID))], "{text}"); + assert_eq!( + cfg["d"], + vec![ + e("claude"), + ge(&format!("codex resume '{ID}'"), "g"), + m("omp", None) + ], + "{text}" + ); } - let (_, cfg) = from_json( - r#"{"d": [{"cmd": "claude", "group": "agents", "name": "pilot"}, - {"cmd": "/usr/local/bin/claude", "name": "mine"}]}"#, - ) - .unwrap(); - assert_eq!( - cfg["d"], - vec![ - SessionEntry { - group: Some("agents".into()), - name: Some("pilot".into()), - ..m("claude", None) - }, - ne("/usr/local/bin/claude", "mine"), - ] - ); } /// Reject the entire file for a managed entry with a `cmd` field, an unregistered @@ -1356,34 +1218,6 @@ mod tests { assert_eq!(list_in(&dir), vec!["old".to_string()]); } - /// Load v1 recovery snapshots as managed entries when the stored strings match the old - /// instrumented forms. Preserve recovery across the upgrade. - #[test] - fn v1_recovery_snapshots_migrate_on_load() { - let base = temp("session_recovery_migrate"); - let rec = recovery_dir(&base); - fs::create_dir_all(&rec).unwrap(); - fs::write( - rec.join("20260714-093015-11.json"), - format!( - r#"{{"version": 1, "name": "autosaved 2026-07-14 09:30", "dirs": {{"~/p": [ - {{"cmd": "claude --resume '{ID}'", "group": "agents"}}, "omp", "cargo test"]}}}}"# - ), - ) - .unwrap(); - assert_eq!( - load_recovery_in(&rec, "20260714-093015-11").unwrap()["~/p"], - vec![ - SessionEntry { - group: Some("agents".into()), - ..m("claude", Some(ID)) - }, - m("omp", None), - e("cargo test"), - ] - ); - } - /// A flat-schema file can be replaced under its filename stem. #[test] fn legacy_file_resaves_under_its_own_stem() { From 374a4e850e0245a6ff013f39bd3d3a358b4e3651 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 10 Oct 2026 16:45:33 -0700 Subject: [PATCH 2/5] Refresh claude support for 2.1.296: logo header label, quadrant-only title frames, rebuilt fixtures --- src/harness/summary.rs | 58 ++++---- src/harness/summary_tests.rs | 139 ++++++++++++------ tests/corpus/README.md | 45 +++--- tests/corpus/preview_claude_action.bin | 32 ++-- tests/corpus/preview_claude_approval.bin | 48 +++--- tests/corpus/preview_claude_body_menu.bin | 22 +-- .../corpus/preview_claude_body_menu_idle.bin | 22 +-- tests/corpus/preview_claude_done.bin | 38 ++--- tests/corpus/preview_claude_idle.bin | 29 ++-- tests/corpus/preview_claude_working.bin | 32 ++-- tests/corpus/preview_claude_working_agent.bin | 40 +++++ tests/corpus/preview_claude_working_tool.bin | 22 +-- 12 files changed, 315 insertions(+), 212 deletions(-) create mode 100644 tests/corpus/preview_claude_working_agent.bin diff --git a/src/harness/summary.rs b/src/harness/summary.rs index e2ab5d1..6960050 100644 --- a/src/harness/summary.rs +++ b/src/harness/summary.rs @@ -141,14 +141,14 @@ impl SummaryAdapter for ClaudeSummary { claude_welcome_label(rows) } - /// Set `working` for braille and quadrant-circle title frames. Treat a - /// [`CLAUDE_SPINNER`] glyph as idle. We observed only `✳` from this family in titles, - /// and only while idle. Return `None` for other shapes or empty payloads. + /// Set `working` for quadrant-circle title frames: 2.1.296 emits `◐ ` and + /// `◑ ` while working. Treat a [`CLAUDE_SPINNER`] glyph as idle: the same + /// release emits `✳ Claude Code`, then `✳ `, at rest. Braille frames were + /// an older claude's title animation (2.1.209) and are rejected. Return `None` for + /// other shapes or empty payloads. fn normalize_title(&self, title: &str) -> Option<(String, bool)> { let rest = after_frame(title, |c| { - CLAUDE_SPINNER.contains(&c) - || braille_frame(c) - || ('\u{25D0}'..='\u{25D3}').contains(&c) + CLAUDE_SPINNER.contains(&c) || ('\u{25D0}'..='\u{25D3}').contains(&c) })?; // An empty payload cannot produce a usable preview. (!rest.is_empty()).then(|| (rest.to_string(), !title.starts_with(CLAUDE_SPINNER))) @@ -297,31 +297,37 @@ fn claude_approval(rows: &[String]) -> Option<(String, &'static str)> { .then(|| (AWAITING_APPROVAL.to_string(), "claude:approval-menu")) } -/// Complete effort values accepted before a welcome-box ellipsis. +/// Complete effort values accepted before a welcome-header ellipsis. const CLAUDE_EFFORT: &[&str] = &["low", "medium", "high", "xhigh", "max"]; -/// `Fable 5 with high effort` from the welcome box → `Fable 5 (high)`. The -/// welcome box is the stable source; user-configurable statusline rows are not -/// parsed. When the box scrolls away, the label is unavailable. +/// `Fable 5.1 with high effort` from the welcome header → `Fable 5.1 (high)`. +/// claude 2.1.296 paints a three-row logo header, logo glyphs left of the text: +/// +/// ```text +/// ▐▛███▛█ Claude Code v2.1.296 +/// ▝▜██████▀ Fable 5.1 with high effort · Claude Max +/// ▝▝ ▝▝ /…/scratchpad/claude-work · /rc +/// ``` +/// +/// The model row follows the `Claude Code v` row, which sits within the first +/// four rows; its first ` · ` segment is the model text. The header is the +/// stable source; user-configurable statusline rows are not parsed. When the +/// header scrolls away, the label is unavailable. fn claude_welcome_label(rows: &[String]) -> Option { - let start = rows + let version = rows .iter() .take(4) - .position(|r| r.trim_start().starts_with("╭─── Claude Code"))?; - for row in &rows[start + 1..] { - if row.trim_start().starts_with('╰') { - break; - } - // First cell of the box row: the welcome pane, left of the divider. - let Some(cell) = row.split('│').nth(1) else { - continue; - }; - let head = cell.trim().split(" · ").next().unwrap_or(""); - if let Some(label) = claude_model_effort(head) { - return Some(label); - } - } - None + .position(|r| claude_header_text(r).starts_with("Claude Code v"))?; + let head = claude_header_text(rows.get(version + 1)?) + .split(" · ") + .next()?; + claude_model_effort(head) +} + +/// The text of a welcome-header row: strip the leading logo glyphs (block +/// elements U+2580 to U+259F) and whitespace. +fn claude_header_text(row: &str) -> &str { + row.trim_start_matches(|c: char| c.is_whitespace() || ('\u{2580}'..='\u{259F}').contains(&c)) } /// Normalize ` with effort` and its ellipsis form to ` diff --git a/src/harness/summary_tests.rs b/src/harness/summary_tests.rs index fc5ae7c..4439159 100644 --- a/src/harness/summary_tests.rs +++ b/src/harness/summary_tests.rs @@ -298,8 +298,8 @@ fn claude_waiting_family_matches_the_skeleton_and_never_probes() { } /// Normalize every recognized frame to the same bare title text. Classify the resting -/// family as idle and braille and quadrant frames as working; reject unsupported shapes -/// and empty payloads. +/// family as idle and quadrant frames as working; reject braille frames (an older +/// claude's animation), unsupported shapes, and empty payloads. #[test] fn claude_title_frames_canonicalize_to_constant_text() { for frame in CLAUDE_SPINNER { @@ -309,30 +309,32 @@ fn claude_title_frames_canonicalize_to_constant_text() { "{frame:?}" ); } + // 2.1.296 names the session once the first turn starts; the frame stays `✳`. + assert_eq!( + ClaudeSummary.normalize_title("✳ Directory listing with ls -la"), + Some(("Directory listing with ls -la".to_string(), false)) + ); // Normalize every working frame to the same title and flag. - let rendered: std::collections::BTreeSet> = BRAILLE_FRAMES - .iter() - .copied() - .chain('\u{25D0}'..='\u{25D3}') + let rendered: std::collections::BTreeSet> = ('\u{25D0}'..='\u{25D3}') .map(|frame| ClaudeSummary.normalize_title(&format!("{frame} Run sleep command"))) .collect(); assert_eq!( rendered, std::collections::BTreeSet::from([Some(("Run sleep command".to_string(), true))]), - "braille and quadrant frames must render one string and report working" + "quadrant frames must render one string and report working" ); - // A braille frame plus the session summary. - assert_eq!( - ClaudeSummary.normalize_title("⠐ Review fleetcom preview design document"), - Some(("Review fleetcom preview design document".to_string(), true)) - ); - assert_eq!( - ClaudeSummary.normalize_title("⠴ Review fleetcom preview design document"), - Some(("Review fleetcom preview design document".to_string(), true)), - "mid-block braille frame" - ); + // Braille frames belong to the 2.1.209 title animation, which 2.1.296 no + // longer emits: refuse rather than guess a state. + for frame in BRAILLE_FRAMES.iter().chain(['⠐'].iter()) { + assert_eq!( + ClaudeSummary + .normalize_title(&format!("{frame} Review fleetcom preview design document")), + None, + "{frame:?}" + ); + } assert_eq!(ClaudeSummary.normalize_title("zellij: main"), None); assert_eq!(ClaudeSummary.normalize_title("✻"), None, "frame alone"); @@ -522,42 +524,74 @@ fn claude_approval_requires_the_dialog_shape() { assert_eq!(ClaudeSummary.live_preview(&lone), None); } -/// The welcome-box label accepts complete and ellipsis forms but rejects a -/// partial effort value. No box means no label. +/// The welcome-header label accepts complete and ellipsis forms but rejects a +/// partial effort value. The model row is the one after `Claude Code v`; no +/// header within the first four rows means no label. #[test] -fn claude_label_reads_the_welcome_box() { - let boxed = |cell: &str| { +fn claude_label_reads_the_logo_header() { + let header = |model_row: &str| { rs(&[ - "╭─── Claude Code v2.1.233 ────────────╮", - cell, - "╰──────────────────────────────────────╯", + "", + " ▐▛███▛█ Claude Code v2.1.296", + model_row, + " ▝▝ ▝▝ ~/Projects/fixture-sources · /rc", ]) }; - let fixed_pane = "│ Opus 5 (1M context) with high… · Claude Max · │ Added opt-in memory cgroup support for Bas… │"; - for (cell, want) in [ + for (model_row, want) in [ ( - "│ Fable 5 with high effort · Claude Max · │ notes │", - Some("Fable 5 (high)"), + "▝▜██████▀ Fable 5.1 with high effort · Claude Max", + Some("Fable 5.1 (high)"), ), // Parentheses in the model name do not change the output shape. ( - "│ Opus 5 (1M context) with high effort · Claude Max · │ notes │", + "▝▜██████▀ Opus 5 (1M context) with high effort · Claude Max", + Some("Opus 5 (1M context) (high)"), + ), + // A narrow terminal cuts the row with the CLI's own ellipsis. + ( + "▝▜██████▀ Opus 5 (1M context) with high…", Some("Opus 5 (1M context) (high)"), ), - (fixed_pane, Some("Opus 5 (1M context) (high)")), // Partial and empty effort values are invalid. - ("│ Opus 5 (1M context) with hi… │ notes │", None), - ("│ Opus 5 (1M context) with … │ notes │", None), + ("▝▜██████▀ Opus 5 (1M context) with hi…", None), + ("▝▜██████▀ Opus 5 (1M context) with …", None), // Neither accepted suffix is present. - ("│ Some Model with high │ notes │", None), + ("▝▜██████▀ Some Model with high", None), ] { assert_eq!( - ClaudeSummary.model_label(&boxed(cell)), + ClaudeSummary.model_label(&header(model_row)), want.map(str::to_string), - "{cell:?}" + "{model_row:?}" ); } - assert_eq!(ClaudeSummary.model_label(&rs(&["no box here"])), None); + + // The model row must follow the version row directly. + let gapped = rs(&[ + " ▐▛███▛█ Claude Code v2.1.296", + "", + "▝▜██████▀ Fable 5.1 with high effort · Claude Max", + ]); + assert_eq!(ClaudeSummary.model_label(&gapped), None); + + // A header below the fourth row has scrolled into the body. + let scrolled = rs(&[ + "", + "", + "", + "", + " ▐▛███▛█ Claude Code v2.1.296", + "▝▜██████▀ Fable 5.1 with high effort · Claude Max", + ]); + assert_eq!(ClaudeSummary.model_label(&scrolled), None); + + // The 2.1.215 welcome box is gone; its grammar is not read. + let boxed = rs(&[ + "╭─── Claude Code v2.1.215 ─────────────────────╮", + "│ Fable 5 with high effort · Claude Max · │ notes │", + "╰──────────────────────────────────────────────╯", + ]); + assert_eq!(ClaudeSummary.model_label(&boxed), None); + assert_eq!(ClaudeSummary.model_label(&rs(&["no header here"])), None); } /// Working-row normalization: parenthetical dropped (unclosed included), @@ -1518,35 +1552,46 @@ fn corpus_positive_states_resolve_exactly() { "preview_claude_working", include_bytes!("../../tests/corpus/preview_claude_working.bin"), &ClaudeSummary, - "Fable 5 (high) · Concocting…", + "Fable 5.1 (high) · Moonwalking…", "claude:spinner", ), Case( "preview_claude_working_tool", include_bytes!("../../tests/corpus/preview_claude_working_tool.bin"), &ClaudeSummary, - "Fable 5 (high) · Hashing…", + "Fable 5.1 (high) · Hashing…", + "claude:spinner", + ), + Case( + "preview_claude_working_agent", + include_bytes!("../../tests/corpus/preview_claude_working_agent.bin"), + &ClaudeSummary, + // The `⎿ Backgrounded agent` attachment above the spinner is not an + // action row, and the roster below the box is outside the scan. + "Fable 5.1 (high) · Hashing…", "claude:spinner", ), Case( "preview_claude_action", include_bytes!("../../tests/corpus/preview_claude_action.bin"), &ClaudeSummary, - "Fable 5 (high) · Running 1 shell command…", + // The spinner row's `thought for 2s` segment rides along; its + // elapsed and token tickers do not. + "Fable 5.1 (high) · Running 1 shell command… · thought for 2s", "claude:action-row", ), Case( "preview_claude_approval", include_bytes!("../../tests/corpus/preview_claude_approval.bin"), &ClaudeSummary, - "Fable 5 (high) · awaiting approval", + "Fable 5.1 (high) · awaiting approval", "claude:approval-menu", ), Case( "preview_claude_tasklist", include_bytes!("../../tests/corpus/preview_claude_tasklist.bin"), &ClaudeSummary, - // Without the welcome box, the preview has no model prefix. + // Without the welcome header, the preview has no model prefix. "Running phase 1 (dashboard UI)…", "claude:spinner", ), @@ -1568,7 +1613,7 @@ fn corpus_positive_states_resolve_exactly() { "preview_claude_waiting", include_bytes!("../../tests/corpus/preview_claude_waiting.bin"), &ClaudeSummary, - // The welcome box is absent, so there is no model prefix. + // The welcome header is absent, so there is no model prefix. "Waiting for 1 background agent to finish", "claude:waiting", ), @@ -1734,7 +1779,11 @@ fn rule_reports_working_classifies_every_emitted_id() { } /// Idle and post-turn screens have no anchor and resolve to the -/// alternate-screen marker. +/// alternate-screen marker. The claude idle screen is the launch state (`Try +/// "…"` placeholder, `● high · /effort` hint row, header still connecting); +/// the done screen has `✻ Sautéed for 17s · done 4:14 PM` as the first +/// column-0 row above a box holding a typed draft: no `…`, so the scan +/// rejects it as foreign. #[test] fn corpus_idle_states_fall_through() { let cases: [(&str, &[u8], &dyn SummaryAdapter); 4] = [ @@ -1796,7 +1845,7 @@ fn corpus_body_shaped_text_never_extracts() { &ClaudeSummary, 120, ); - assert_eq!(got, anchor("Fable 5 (high) · Hashing…", "claude:spinner")); + assert_eq!(got, anchor("Fable 5.1 (high) · Hashing…", "claude:spinner")); // Menu touching the chrome window on an idle screen: abort, marker. let got = corpus( @@ -1863,7 +1912,7 @@ fn corpus_truncated_rows_still_anchor() { &ClaudeSummary, 80, ); - // With no welcome box on the narrow screen, omit the label. + // With no welcome header on the narrow screen, omit the label. assert_eq!(got, anchor("Hashing…", "claude:spinner")); let got = corpus( diff --git a/tests/corpus/README.md b/tests/corpus/README.md index bd2778d..0919e4b 100644 --- a/tests/corpus/README.md +++ b/tests/corpus/README.md @@ -45,12 +45,20 @@ Geometry is 40×80 for `preview_codex_reasoning.bin` and 40×36 for reproduce the inline layout. Indentation in the Codex hint-row and approval fixtures is approximate; match -trimmed heads and column-0 structure in their tests. In the Claude waiting -fixture, the welcome box is omitted and agent-roster rows are included below the -input box. In the Claude task-list fixtures, generic phase names are used in a -task-list layout and the welcome box is omitted. In the Claude workflow-wait -fixture, generic wording is used, the welcome box is omitted, and a long blank -gap is included above the input box with a roster below it. The omp fixtures are +trimmed heads and column-0 structure in their tests. The Claude fixtures with a +model prefix open on claude 2.1.296's three-row logo header (`Claude Code +v2.1.296`, `Fable 5.1 with high effort · Claude Max`, working directory); the +working directory on the header row and on the statusline row below the input +box is a same-width synthetic path. Fixtures captured on 2.1.296 omit the +capture environment's `⚠ Transcript saving is off` statusline row, leaving it +empty; the three fixtures whose body is from 2.1.215 carry the 2.1.296 header +transplanted over the old welcome box, with the box's remaining rows left empty +so the body keeps its row indexes. In the Claude waiting fixture, the header is +omitted and agent-roster rows are included below the input box. In the Claude +task-list fixtures, generic phase names are used in a task-list layout and the +header is omitted. In the Claude workflow-wait fixture, generic wording is used, +the header is omitted, and a long blank gap is included above the input box +with a roster below it. The omp fixtures are the 18.8.7 default layout: logo and tip, the status band (`π >` idle, `⠇ 4s >` working), and a `╰─` composer gutter with no top border. The working directory in the band and the tool-call path in the body are replaced with same-width @@ -66,16 +74,17 @@ prevent a byte-for-byte match with the captured header. | Fixture | Scenario | Coverage | | --- | --- | --- | -| `preview_claude_working.bin` | claude spinner with the tmux focus-events hint row | `claude:spinner` extraction through an indented hint row | -| `preview_claude_working_tool.bin` | claude spinner over an indented tool-attachment row | `claude:spinner`; the attachment row is not the action row | -| `preview_claude_action.bin` | claude `⏺ Running 1 shell command…` above the spinner | `claude:action-row` preferred over the rotating verb | -| `preview_claude_approval.bin` | claude file-write approval dialog, input box replaced | `awaiting approval` synthesized by `claude:approval-menu` | -| `preview_claude_idle.bin` | claude idle with the `Try "…"` placeholder | fall-through to the marker; the placeholder excluded from anchors | -| `preview_claude_done.bin` | claude after a finished turn (`✻ Crunched for 4s` in the body) | fall-through; body completion rows are out of the pinned window | -| `preview_claude_body_menu.bin` | approval-menu text quoted in the body while the spinner runs | the pinned spinner preferred; body menu text is ignored | -| `preview_claude_body_menu_idle.bin` | approval-menu text touching the chrome window, input box intact | extraction aborted at a foreign column-0 row; marker fallback | -| `preview_claude_tasklist.bin` | claude spinner above a six-row task-list block, agent roster below the box | `claude:spinner` through a bounded indented block | -| `preview_claude_body_above_tasklist.bin` | spinner-shaped body row above `⏺` prose and the task list | negative: status structure rejected on column-0 prose | +| `preview_claude_working.bin` | claude 2.1.296, captured 2026-10-10: spinner over the right-aligned `● high · /effort` hint row | `claude:spinner` extraction through an indented hint row | +| `preview_claude_working_tool.bin` | body from 2.1.215, header transplanted from 2.1.296: spinner over an indented tool-attachment row | `claude:spinner`; the attachment row is not the action row | +| `preview_claude_working_agent.bin` | claude 2.1.296, captured 2026-10-10: spinner over a backgrounded `⏺ Agent(…)` row and its `⎿` attachment, agent roster below the box | `claude:spinner`; the attachment is not the action row, the roster is outside the scan | +| `preview_claude_action.bin` | claude 2.1.296, captured 2026-10-10: `⏺ Running 1 shell command…` above a `· Whisking… (9s · ↓ 261 tokens · thought for 2s)` spinner | `claude:action-row` preferred over the rotating verb; `thought for 2s` kept, tickers dropped | +| `preview_claude_approval.bin` | claude 2.1.296, captured 2026-10-10: Bash command approval dialog, input box replaced, four-option menu | `awaiting approval` synthesized by `claude:approval-menu` | +| `preview_claude_idle.bin` | claude 2.1.296, captured 2026-10-10: launch state with the `Try "…"` placeholder, `/rc connecting…` on the header, no statusline rows yet | fall-through to the marker; the placeholder excluded from anchors | +| `preview_claude_done.bin` | claude 2.1.296, captured 2026-10-10: finished turn (`✻ Sautéed for 17s · done 4:14 PM` in the body) with a typed draft in the box | fall-through; the ellipsis-less completion row is rejected as a foreign column-0 row | +| `preview_claude_body_menu.bin` | body from 2.1.215, header transplanted from 2.1.296: approval-menu text quoted in the body while the spinner runs | the pinned spinner preferred; body menu text is ignored | +| `preview_claude_body_menu_idle.bin` | body from 2.1.215, header transplanted from 2.1.296: approval-menu text touching the chrome window, input box intact | extraction aborted at a foreign column-0 row; marker fallback | +| `preview_claude_tasklist.bin` | 2.1.215, not re-observed on 2.1.296: spinner above a six-row task-list block, agent roster below the box | `claude:spinner` through a bounded indented block | +| `preview_claude_body_above_tasklist.bin` | 2.1.215, not re-observed on 2.1.296: spinner-shaped body row above `⏺` prose and the task list | negative: status structure rejected on column-0 prose | | `preview_codex_working.bin` | codex `• Working (7s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close` | `codex:working` normalization: affordances stripped, slow suffix kept | | `preview_codex_working_over_ran.bin` | codex working with a `• Ran` row higher in the same turn | `codex:working` preferred at the pin; stale row excluded | | `preview_codex_scrollback.bin` | codex finished turn, `• Ran` from the prior turn in scrollback | scan stopped at the reply bullet; floor-tier fallback | @@ -85,8 +94,8 @@ prevent a byte-for-byte match with the captured header. | `preview_codex_reasoning.bin` | codex status row with a reasoning phrase over an `• Explored` group and reply bullet; composer carrying text; no status line | header kept verbatim by `codex:working`; preceding `•`-headed rows excluded; no model prefix without a status line | | `preview_codex_queued.bin` | codex `• Working` row separated from the composer by a `• Queued follow-up inputs` block; `model-with-reasoning · current-dir` status line below | `codex:working` extracted across queued-message heads; model label from the first status-line item | | `preview_codex_body_menu.bin` | modal-shaped menu quoted in the body, live composer below | negative: modal match suppressed with composer present; floor-tier fallback | -| `preview_claude_waiting.bin` | claude waiting on a backgrounded subagent, `⏺` prose and agent roster around the box | ellipsis-less row extracted verbatim by `claude:waiting`; no model label mid-session | -| `preview_claude_workflow_wait.bin` | claude waiting on a dynamic workflow, with 19 blank rows before the input box and a workflow roster below it | blank rows skipped during `claude:waiting` matching; the roster is excluded | +| `preview_claude_waiting.bin` | 2.1.215, not re-observed on 2.1.296, where the `✻ Waiting for 1 background agent to finish` row was seen only in the body after the turn: waiting on a backgrounded subagent, `⏺` prose and agent roster around the box | ellipsis-less row extracted verbatim by `claude:waiting`; no model label mid-session | +| `preview_claude_workflow_wait.bin` | 2.1.215, not re-observed on 2.1.296: waiting on a dynamic workflow, with 19 blank rows before the input box and a workflow roster below it | blank rows skipped during `claude:waiting` matching; the roster is excluded | | `preview_grok_working.bin` | grok braille spinner with elapsed/throughput ticker | `grok:spinner` cut at the label's `…`; border label read | | `preview_grok_worked.bin` | grok `Worked for 8.7s` completion row above the box | `grok:worked` kept verbatim | | `preview_grok_still_running.bin` | grok `◎ 1 subagent still running` above the idle box | `grok:still-running`; border label read | diff --git a/tests/corpus/preview_claude_action.bin b/tests/corpus/preview_claude_action.bin index 7dce920..2bcbab0 100644 --- a/tests/corpus/preview_claude_action.bin +++ b/tests/corpus/preview_claude_action.bin @@ -1,18 +1,17 @@ [?1049h -╭─── Claude Code v2.1.215 ─────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ Tips for getting started │ -│ Welcome back Fleet Pilot! │ Run /init to create a CLAUDE.md file with instructions for Cla… │ -│ │ ─────────────────────────────────────────────────────────────── │ -│ ▐▛███▜▌ │ What's new │ -│ ▝▜█████▛▘ │ Claude no longer runs the `/verify` and `/code-review` skills … │ -│ ▘▘ ▝▝ │ Fixed single-segment `dir/**` allow rules like `Edit(src/**)` … │ -│ Fable 5 with high effort · Claude Max · │ Fixed a permission-check bypass affecting commands run in Wind… │ -│ anonymized-account0@example.invalid's Organization │ /release-notes for more │ -│ ~/Projects/fixture-sources │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ▐▛███▛█ Claude Code v2.1.296 +▝▜██████▀ Fable 5.1 with high effort · Claude Max + ▝▝ ▝▝ /…/Users/fleet-pilot/Projects/fixture-sources/ptys/claude-work · /rc -❯ run: sleep 5 && echo ok +❯ Use the Bash tool to run ls -la and tell me the file names. + +⏺ Listing the working directory now. + + Listed 1 directory + +⏺ The listing came back empty, which is unusual since ls -la always prints . and ... Checking the working directory + directly. ⏺ Running 1 shell command… @@ -31,10 +30,11 @@ -· Hashing… (3s · ↓ 52 tokens) - tmux focus-events off · add 'set -g focus-events on' to ~/.tmux.conf and reattach for focus tracking +· Whisking… (9s · ↓ 261 tokens · thought for 2s) + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ❯  ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── - (statusline scrubbed: user-configured row) - ⏸ manual mode on · ← 2 agents \ No newline at end of file + + Fable 5.1 (high) | /Users/fleet-pilot/Projects/fixture-sources/corpus-capture/claude-2-1-296/sessions/fixture-pty-s… + ⏵⏵ auto mode on (shift+tab to cycle) \ No newline at end of file diff --git a/tests/corpus/preview_claude_approval.bin b/tests/corpus/preview_claude_approval.bin index 4b2a9ce..3e3d9d0 100644 --- a/tests/corpus/preview_claude_approval.bin +++ b/tests/corpus/preview_claude_approval.bin @@ -1,39 +1,39 @@ [?1049h -╭─── Claude Code v2.1.215 ─────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ Tips for getting started │ -│ Welcome back Fleet Pilot! │ Run /init to create a CLAUDE.md file with instructions for Cla… │ -│ │ ─────────────────────────────────────────────────────────────── │ -│ ▐▛███▜▌ │ What's new │ -│ ▝▜█████▛▘ │ Claude no longer runs the `/verify` and `/code-review` skills … │ -│ ▘▘ ▝▝ │ Fixed single-segment `dir/**` allow rules like `Edit(src/**)` … │ -│ Fable 5 with high effort · Claude Max · │ Fixed a permission-check bypass affecting commands run in Wind… │ -│ anonymized-account0@example.invalid's Organization │ /release-notes for more │ -│ ~/Projects/fixture-sources │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ▐▛███▛█ Claude Code v2.1.296 +▝▜██████▀ Fable 5.1 with high effort · Claude Max + ▝▝ ▝▝ /…/Users/fleet-pilot/Projects/fixture-sources/ptys/claude-work · /rc -❯ echo ok +❯ Use the Bash tool to run ls -la and tell me the file names. - Ran 1 shell command +⏺ Listing the working directory now. -⏺ ok + Listed 1 directory -✻ Crunched for 4s +⏺ ls -la printed nothing, which it never does for an existing directory. Checking the working directory and listing it + by explicit path. -❯ write a file to the current directory with an interesting word as its content - -⏺ Write(word.txt) + Listing the working directory by absolute path and showing exit status + ⎿ $ ls -la "/Users/fleet-pilot/Projects/fixture-sources/corpus-capture/claude-2-1-296/sessions/fixture-pty-session/sc + ratchpad/claude-work" 2>&1; echo "exit=$?" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── - Create file - word.txt + Bash command + Tip: auto mode handles these prompts for you — choose "switch to auto mode" below + List the working directory by absolute path and show exit status ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ - 1 petrichor + │ ls -la "/Users/fleet-pilot/Projects/fixture-sources/corpus-capture/claude-2-1-296/sessions/fixture-pty-session/scrat + │ chpad/claude-work" 2>&1; echo "exit=$?" ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ - Do you want to create word.txt? + This command requires approval + + Do you want to proceed? ❯ 1. Yes - 2. Yes, allow all edits during this session (shift+tab) - 3. No + 2. Yes, and don’t ask again for: echo "exit=$?" + 3. Yes, and switch to auto mode · auto mode handles these prompts for you + 4. No Esc to cancel · Tab to amend + + diff --git a/tests/corpus/preview_claude_body_menu.bin b/tests/corpus/preview_claude_body_menu.bin index a8a0c81..63f5bdf 100644 --- a/tests/corpus/preview_claude_body_menu.bin +++ b/tests/corpus/preview_claude_body_menu.bin @@ -1,15 +1,15 @@ [?1049h -╭─── Claude Code v2.1.215 ─────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ Tips for getting started │ -│ Welcome back Fleet Pilot! │ Run /init to create a CLAUDE.md file with instructions for Cla… │ -│ │ ─────────────────────────────────────────────────────────────── │ -│ ▐▛███▜▌ │ What's new │ -│ ▝▜█████▛▘ │ Claude no longer runs the `/verify` and `/code-review` skills … │ -│ ▘▘ ▝▝ │ Fixed single-segment `dir/**` allow rules like `Edit(src/**)` … │ -│ Fable 5 with high effort · Claude Max · │ Fixed a permission-check bypass affecting commands run in Wind… │ -│ anonymized-account0@example.invalid's Organization │ /release-notes for more │ -│ ~/Projects/fixture-sources │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ▐▛███▛█ Claude Code v2.1.296 +▝▜██████▀ Fable 5.1 with high effort · Claude Max + ▝▝ ▝▝ /…/Users/fleet-pilot/Projects/fixture-sources/ptys/claude-work · /rc + + + + + + + + ❯ run: sleep 5 && echo ok diff --git a/tests/corpus/preview_claude_body_menu_idle.bin b/tests/corpus/preview_claude_body_menu_idle.bin index 19ae209..63782bc 100644 --- a/tests/corpus/preview_claude_body_menu_idle.bin +++ b/tests/corpus/preview_claude_body_menu_idle.bin @@ -1,15 +1,15 @@ [?1049h -╭─── Claude Code v2.1.215 ─────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ Tips for getting started │ -│ Welcome back Fleet Pilot! │ Run /init to create a CLAUDE.md file with instructions for Cla… │ -│ │ ─────────────────────────────────────────────────────────────── │ -│ ▐▛███▜▌ │ What's new │ -│ ▝▜█████▛▘ │ Claude no longer runs the `/verify` and `/code-review` skills … │ -│ ▘▘ ▝▝ │ Fixed single-segment `dir/**` allow rules like `Edit(src/**)` … │ -│ Fable 5 with high effort · Claude Max · │ Fixed a permission-check bypass affecting commands run in Wind… │ -│ anonymized-account0@example.invalid's Organization │ /release-notes for more │ -│ ~/Projects/fixture-sources │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ▐▛███▛█ Claude Code v2.1.296 +▝▜██████▀ Fable 5.1 with high effort · Claude Max + ▝▝ ▝▝ /…/Users/fleet-pilot/Projects/fixture-sources/ptys/claude-work · /rc + + + + + + + + ❯ echo ok diff --git a/tests/corpus/preview_claude_done.bin b/tests/corpus/preview_claude_done.bin index a153ba9..83ad0a6 100644 --- a/tests/corpus/preview_claude_done.bin +++ b/tests/corpus/preview_claude_done.bin @@ -1,29 +1,29 @@ [?1049h -╭─── Claude Code v2.1.215 ─────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ Tips for getting started │ -│ Welcome back Fleet Pilot! │ Run /init to create a CLAUDE.md file with instructions for Cla… │ -│ │ ─────────────────────────────────────────────────────────────── │ -│ ▐▛███▜▌ │ What's new │ -│ ▝▜█████▛▘ │ Claude no longer runs the `/verify` and `/code-review` skills … │ -│ ▘▘ ▝▝ │ Fixed single-segment `dir/**` allow rules like `Edit(src/**)` … │ -│ Fable 5 with high effort · Claude Max · │ Fixed a permission-check bypass affecting commands run in Wind… │ -│ anonymized-account0@example.invalid's Organization │ /release-notes for more │ -│ ~/Projects/fixture-sources │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ▐▛███▛█ Claude Code v2.1.296 +▝▜██████▀ Fable 5.1 with high effort · Claude Max + ▝▝ ▝▝ /…/Users/fleet-pilot/Projects/fixture-sources/ptys/claude-work · /rc -❯ echo ok +❯ Use the Bash tool to run ls -la and tell me the file names. - Ran 1 shell command +⏺ Listing the working directory now. -⏺ ok + Listed 1 directory -✻ Crunched for 4s +⏺ The listing came back empty, which is unusual since ls -la always prints . and ... Checking the working directory + directly. + Ran 1 shell command +⏺ The directory holds a single file: + - README.md (7 bytes, modified today at 15:32) + The first ls -la run produced no output at all, which is why I re-ran it by absolute path. Your ls is aliased to a + tool that uses a different output format and omits the . and .. entries, which fits the second listing. The initial + empty result most likely came from that alias misbehaving without a terminal attached. Nothing was changed. +✻ Sautéed for 17s · done 4:14 PM @@ -32,9 +32,9 @@ - tmux focus-events off · add 'set -g focus-events on' to ~/.tmux.conf and reattach for focus tracking ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -❯  +❯ what's in README.md ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── - (statusline scrubbed: user-configured row) - ⏸ manual mode on · ← 2 agents \ No newline at end of file + + Fable 5.1 (high) | /Users/fleet-pilot/Projects/fixture-sources/corpus-capture/claude-2-1-296/sessions/fixture-pty-s… + ⏵⏵ auto mode on (shift+tab to cycle) \ No newline at end of file diff --git a/tests/corpus/preview_claude_idle.bin b/tests/corpus/preview_claude_idle.bin index 95604ab..b460389 100644 --- a/tests/corpus/preview_claude_idle.bin +++ b/tests/corpus/preview_claude_idle.bin @@ -1,15 +1,7 @@ [?1049h -╭─── Claude Code v2.1.215 ─────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ Tips for getting started │ -│ Welcome back Fleet Pilot! │ Run /init to create a CLAUDE.md file with instructions for Cla… │ -│ │ ─────────────────────────────────────────────────────────────── │ -│ ▐▛███▜▌ │ What's new │ -│ ▝▜█████▛▘ │ Claude no longer runs the `/verify` and `/code-review` skills … │ -│ ▘▘ ▝▝ │ Fixed single-segment `dir/**` allow rules like `Edit(src/**)` … │ -│ Fable 5 with high effort · Claude Max · │ Fixed a permission-check bypass affecting commands run in Wind… │ -│ anonymized-account0@example.invalid's Organization │ /release-notes for more │ -│ ~/Projects/fixture-sources │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ▐▛███▛█ Claude Code v2.1.296 +▝▜██████▀ Fable 5.1 with high effort · Claude Max + ▝▝ ▝▝ /…/Users/fleet-pilot/Projects/fixture-sources/ptys/claude-work · /rc connecting… @@ -32,9 +24,16 @@ - ● high · /effort + + + + + + + + ● high · /effort ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -❯ Try "refactor " +❯ Try "how do I log an error?" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── - (statusline scrubbed: user-configured row) - ⏸ manual mode on · ← 2 agents \ No newline at end of file + + diff --git a/tests/corpus/preview_claude_working.bin b/tests/corpus/preview_claude_working.bin index 8805269..be2e2ed 100644 --- a/tests/corpus/preview_claude_working.bin +++ b/tests/corpus/preview_claude_working.bin @@ -1,18 +1,10 @@ [?1049h -╭─── Claude Code v2.1.215 ─────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ Tips for getting started │ -│ Welcome back Fleet Pilot! │ Run /init to create a CLAUDE.md file with instructions for Cla… │ -│ │ ─────────────────────────────────────────────────────────────── │ -│ ▐▛███▜▌ │ What's new │ -│ ▝▜█████▛▘ │ Claude no longer runs the `/verify` and `/code-review` skills … │ -│ ▘▘ ▝▝ │ Fixed single-segment `dir/**` allow rules like `Edit(src/**)` … │ -│ Fable 5 with high effort · Claude Max · │ Fixed a permission-check bypass affecting commands run in Wind… │ -│ anonymized-account0@example.invalid's Organization │ /release-notes for more │ -│ ~/Projects/fixture-sources │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ▐▛███▛█ Claude Code v2.1.296 +▝▜██████▀ Fable 5.1 with high effort · Claude Max + ▝▝ ▝▝ /…/Users/fleet-pilot/Projects/fixture-sources/ptys/claude-work · /rc -❯ run: sleep 5 && echo ok +❯ Reply with exactly the word ok and nothing else. @@ -31,10 +23,18 @@ -✽ Concocting… - tmux focus-events off · add 'set -g focus-events on' to ~/.tmux.conf and reattach for focus tracking + + + + + + + +✶ Moonwalking… + ● high · /effort ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ❯  ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── - (statusline scrubbed: user-configured row) - ⏸ manual mode on · ← 2 agents \ No newline at end of file + + Fable 5.1 (high) | /Users/fleet-pilot/Projects/fixture-sources/corpus-capture/claude-2-1-296/sessions/fixture-pty-s… + ⏵⏵ auto mode on (shift+tab to cycle) \ No newline at end of file diff --git a/tests/corpus/preview_claude_working_agent.bin b/tests/corpus/preview_claude_working_agent.bin new file mode 100644 index 0000000..577afe7 --- /dev/null +++ b/tests/corpus/preview_claude_working_agent.bin @@ -0,0 +1,40 @@ +[?1049h + ▐▛███▛█ Claude Code v2.1.296 +▝▜██████▀ Fable 5.1 with high effort · Claude Max + ▝▝ ▝▝ /…/Users/fleet-pilot/Projects/fixture-sources/ptys/claude-work · /rc + + +❯ Use the Task tool to launch one subagent whose only job is to reply with the word ok, then tell me what it said. + +⏺ Launching one subagent now. + +⏺ Agent(Reply with the word ok) + ⎿  Backgrounded agent (↓ to manage · ctrl+o to expand) + + + + + + + + + + + + + + + + + +✽ Hashing… (2s · ↓ 112 tokens) + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +❯  +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Fable 5.1 (high) | /Users/fleet-pilot/Projects/fixture-sources/corpus-capture/claude-2-1-296/sessions/fixture-pty-s… + ⏵⏵ auto mode on (shift+tab to cycle) + + ⏺ main + ◯ general-purpose Reply with the word ok 0s \ No newline at end of file diff --git a/tests/corpus/preview_claude_working_tool.bin b/tests/corpus/preview_claude_working_tool.bin index 154deda..33abda4 100644 --- a/tests/corpus/preview_claude_working_tool.bin +++ b/tests/corpus/preview_claude_working_tool.bin @@ -1,15 +1,15 @@ [?1049h -╭─── Claude Code v2.1.215 ─────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ Tips for getting started │ -│ Welcome back Fleet Pilot! │ Run /init to create a CLAUDE.md file with instructions for Cla… │ -│ │ ─────────────────────────────────────────────────────────────── │ -│ ▐▛███▜▌ │ What's new │ -│ ▝▜█████▛▘ │ Claude no longer runs the `/verify` and `/code-review` skills … │ -│ ▘▘ ▝▝ │ Fixed single-segment `dir/**` allow rules like `Edit(src/**)` … │ -│ Fable 5 with high effort · Claude Max · │ Fixed a permission-check bypass affecting commands run in Wind… │ -│ anonymized-account0@example.invalid's Organization │ /release-notes for more │ -│ ~/Projects/fixture-sources │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ▐▛███▛█ Claude Code v2.1.296 +▝▜██████▀ Fable 5.1 with high effort · Claude Max + ▝▝ ▝▝ /…/Users/fleet-pilot/Projects/fixture-sources/ptys/claude-work · /rc + + + + + + + + ❯ run: sleep 5 && echo ok From 106867bf15511eb62a64d459340c581ca526230b Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 10 Oct 2026 17:03:30 -0700 Subject: [PATCH 3/5] Refresh codex support for 0.162.1: title grammar, codex:worked arm, legacy arms and 0.157.1 streams deleted, rebuilt fixtures --- docs/agent-resume.md | 2 +- src/harness/codex.rs | 29 +- src/harness/summary.rs | 72 +++- src/harness/summary_tests.rs | 392 ++++++++++-------- src/terminal/golden.rs | 8 +- tests/corpus/README.md | 53 ++- .../0.157.1-fullscreen-approval.bin | 1 - .../0.157.1-fullscreen-stdin.bin | 1 - .../codex_0158_preview/0.157.1-inline.bin | 17 - tests/corpus/codex_0158_preview/README.md | 25 +- .../0.157.1-fullscreen-fleetcom-load.bin | 1 - .../0.157.1-fullscreen-transitions.bin | 1 - tests/corpus/codex_0158_terminal/README.md | 7 +- .../corpus/codex_0158_terminal/manifest.json | 110 ----- tests/corpus/preview_codex_done.bin | 40 ++ tests/corpus/preview_codex_idle.bin | 40 ++ tests/corpus/preview_codex_queued.bin | 38 +- tests/corpus/preview_codex_ran_done.bin | 40 ++ tests/corpus/preview_codex_working.bin | 35 +- 19 files changed, 475 insertions(+), 437 deletions(-) delete mode 100644 tests/corpus/codex_0158_preview/0.157.1-fullscreen-approval.bin delete mode 100644 tests/corpus/codex_0158_preview/0.157.1-fullscreen-stdin.bin delete mode 100644 tests/corpus/codex_0158_preview/0.157.1-inline.bin delete mode 100644 tests/corpus/codex_0158_terminal/0.157.1-fullscreen-fleetcom-load.bin delete mode 100644 tests/corpus/codex_0158_terminal/0.157.1-fullscreen-transitions.bin create mode 100644 tests/corpus/preview_codex_done.bin create mode 100644 tests/corpus/preview_codex_idle.bin create mode 100644 tests/corpus/preview_codex_ran_done.bin diff --git a/docs/agent-resume.md b/docs/agent-resume.md index 0d7953d..96ef1ad 100644 --- a/docs/agent-resume.md +++ b/docs/agent-resume.md @@ -105,7 +105,7 @@ Live validation on 2026-09-28 was performed with the official macOS arm64 Codex The [recorded terminal fixtures](../tests/corpus/codex_0158_terminal/README.md) preserve resumed-history and clipboard output from those runs. Offline replay checks the recorded display and OSC 52 forwarding; it does not run the notifier or execute save/load. The separate `tests/daemon_resume.rs` integration tests exercise session resume with stub agents. -Live validation on 2026-10-04 was performed with codex-cli 0.160.0 over nine TUI sessions. Three thread IDs were reported from one process: the unsaved title thread, a sub-agent, and the root. The title thread's notification arrived before the root's in three sessions and after it in six, and no rollout appeared for it even minutes later. For a root or sub-agent turn, the thread's rollout header was on disk at least 2.5 s before its notification, and `task_complete` was recorded 21–256 ms before it. When resuming the title thread, "No saved session found" was reported; for the sub-agent, "cannot resume an unloaded multi-agent v2 sub-agent through its parent" was reported. With the notify override alone, `⚠ 1 warning` was displayed in the footer; with both overrides, none was displayed. With an unknown `features.*` key, startup completed with an unrecognized-setting warning. No Codex version predating `daemon_auto_start` was tested. On the same date, 493 local rollout headers from Codex 0.135.0 through 0.160.0 were examined: `id` and `session_id` were present in each. In Codex 0.140.0 and 0.141.0, a sub-agent's own ID was stored as its `session_id`; that format is rejected. +Live validation on 2026-10-04 was performed with codex-cli 0.160.0 over nine TUI sessions. Three thread IDs were reported from one process: the unsaved title thread, a sub-agent, and the root. The title thread's notification arrived before the root's in three sessions and after it in six, and no rollout appeared for it even minutes later. For a root or sub-agent turn, the thread's rollout header was on disk at least 2.5 s before its notification, and `task_complete` was recorded 21–256 ms before it. When resuming the title thread, "No saved session found" was reported; for the sub-agent, "cannot resume an unloaded multi-agent v2 sub-agent through its parent" was reported. With the notify override alone, `⚠ 1 warning` was displayed in the footer; with both overrides, none was displayed. With an unknown `features.*` key, startup completed with an unrecognized-setting warning. No Codex version predating `daemon_auto_start` was tested. ### `grok` diff --git a/src/harness/codex.rs b/src/harness/codex.rs index 06a7294..b9f4ef1 100644 --- a/src/harness/codex.rs +++ b/src/harness/codex.rs @@ -177,8 +177,8 @@ fn replace_slot(capture: &Path, root: &str) -> io::Result<()> { /// `source` object. /// /// Return `None` unless exactly one rollout is named for `thread` and its -/// header is in one of those two formats. No rollout is saved for the title -/// thread. Examine only directory listings and the first line. +/// header names `thread` itself or a sub-agent's root. No rollout is saved +/// for the title thread. Examine only directory listings and the first line. fn root_thread(home: &Path, thread: &str) -> Option { let mut rollouts = Vec::new(); collect_rollouts( @@ -197,9 +197,8 @@ fn root_thread(home: &Path, thread: &str) -> Option { return None; } let root = capture_id(meta, "session_id")?; - // Require exactly one condition. In codex 0.140.0 and 0.141.0, a - // sub-agent's own ID was stored as `session_id`, so its root is unknown. - ((root == thread) != meta["source"].has_key("subagent")).then_some(root) + // A foreign root is trusted only from a sub-agent's header. + (root == thread || meta["source"].has_key("subagent")).then_some(root) } /// Push every entry with a name ending in `suffix`, exactly `depth` directory @@ -1027,26 +1026,6 @@ mod tests { assert_eq!(resolve(&home, CHILD).as_deref(), Some(ID)); } - /// In codex 0.140.0 and 0.141.0, a sub-agent's own ID was stored as - /// `session_id`. Reject `id == session_id` with a `subagent` source: - /// the root is unknown, and the sub-agent's ID is not resumable. - #[test] - fn arrival_root_refuses_a_sub_agent_that_names_itself_as_root() { - let home = temp("codex_capture_self_rooted"); - for source in [ - thread_spawn(ID, 1), - r#"{"subagent":{"other":"guardian"}}"#.to_string(), - r#"{"subagent":"review"}"#.to_string(), - ] { - install_codex_rollout( - &home, - CHILD, - codex_session_meta(&child_meta(CHILD, ID, CHILD, &source)), - ); - assert_eq!(resolve(&home, CHILD), None, "{source}"); - } - } - /// No rollout is saved for the title thread. Reject its notification /// with an absent store, an empty store, or rollouts for other threads. #[test] diff --git a/src/harness/summary.rs b/src/harness/summary.rs index 6960050..64c5830 100644 --- a/src/harness/summary.rs +++ b/src/harness/summary.rs @@ -53,6 +53,7 @@ pub fn rule_reports_working(rule: &str) -> Option { | "claude:waiting" | "codex:approval-menu" | "codex:ran" + | "codex:worked" | "grok:worked" | "grok:still-running" => Some(false), _ => None, @@ -388,17 +389,41 @@ impl SummaryAdapter for CodexSummary { codex_model_label(rows) } - /// Set `working` for a title led by a braille frame. Replace `[ . ] ` with `[ ! ] ` - /// to show the blocked blink's canonical phase. Return `None` for other title shapes - /// and for an empty frame payload. + /// Codex 0.162.1 titles the terminal `[head ]summary | cwd`, where `cwd` is + /// the working directory's basename and `head` is a braille frame or the + /// blocked blink `[ . ] `/`[ ! ] `. One turn shows four titles: `codex-work` + /// at startup, `⠴ ⠴ | codex-work` for the first seconds of the turn (the + /// summary is a frame placeholder until the turn is named), `⠙ Count to + /// twenty slowly | codex-work` while working, `Count to twenty slowly | + /// codex-work` after. Return the summary, split at the last ` | `, with + /// `working` set for a frame head; fold the blink to its `[ ! ] ` phase. + /// Return `None` for the bare cwd (the alternate screen keeps it verbatim) + /// and for a placeholder or empty summary: the working signal is lost for + /// the placeholder seconds rather than a frame rendering as text. A headed + /// title without ` | ` is all summary. fn normalize_title(&self, title: &str) -> Option<(String, bool)> { - if let Some(rest) = title.strip_prefix("[ . ] ") { - return Some((format!("[ ! ] {rest}"), false)); + let frame = after_frame(title, braille_frame); + let blink = title + .strip_prefix("[ . ] ") + .or_else(|| title.strip_prefix("[ ! ] ")); + let rest = frame.or(blink).unwrap_or(title); + let summary = match rest.rsplit_once(" | ") { + Some((summary, _)) => summary, + None if frame.is_some() || blink.is_some() => rest, + None => return None, + }; + let mut chars = summary.chars(); + match chars.next() { + None => return None, + Some(c) if braille_frame(c) && chars.next().is_none() => return None, + Some(_) => {} } - // Do not normalize a frame with an empty payload; Claude and OMP use the same rule. - after_frame(title, braille_frame) - .filter(|rest| !rest.is_empty()) - .map(|rest| (rest.to_string(), true)) + let text = if blink.is_some() { + format!("[ ! ] {summary}") + } else { + summary.to_string() + }; + Some((text, frame.is_some())) } } @@ -515,11 +540,9 @@ fn codex_warning_shortcut(hint: &str) -> bool { for modifier in ["ctrl+", "shift+"] { key = key.strip_prefix(modifier).unwrap_or(key); } - // Since Codex 0.160.0, `⌥w` is displayed instead of `⌥+w`. - // Treat a trailing `+` as the key itself. + // `⌥` is glued to its key (`⌥w`), unlike the `+`-joined modifiers. key = key .strip_prefix("alt+") - .or_else(|| key.strip_prefix("⌥+").filter(|rest| !rest.is_empty())) .or_else(|| key.strip_prefix('⌥')) .unwrap_or(key); (key.len() == 1 && !key.starts_with(char::is_whitespace)) @@ -581,11 +604,12 @@ fn codex_composer(rows: &[String]) -> Option { /// Walk up from the composer through the status region: blanks and indented rows /// (tool-output attachments like `└ ok`, wrapped continuations) are skipped, /// [`CODEX_QUEUED_HEADS`] are walked past, then inspect the first other column-0 row. -/// Extract only [`codex_status_head`] or `• Ran ` shapes; stop at any other column-0 -/// row (a reply bullet, a `⚠` notice, a turn separator): `• Ran` rows from prior turns -/// are retained in scrollback, and skipping an unknown row to reach one would resurface -/// stale work as live status. `• Ran ` is tested first because the status head matches -/// on structure, not on a literal verb. +/// Extract only [`codex_worked`], [`codex_status_head`], or `• Ran ` shapes; stop at +/// any other column-0 row (a reply bullet, a `⚠` notice, a turn separator): `• Ran` +/// rows from prior turns are retained in scrollback, and skipping an unknown row to +/// reach one would resurface stale work as live status. The completion row is +/// indented, so it is tested before the indentation skip. `• Ran ` is tested before +/// the status head because the head matches on structure, not on a literal verb. fn codex_status(rows: &[String], composer: usize) -> Option<(String, &'static str)> { // Indented rows crossed since the last column-0 row. Exclude rows below a queued // head from the count to avoid exhausting the window on a deep queue. @@ -594,6 +618,9 @@ fn codex_status(rows: &[String], composer: usize) -> Option<(String, &'static st if row.is_empty() { continue; } + if let Some(text) = codex_worked(row) { + return (indented <= CODEX_STATUS_WINDOW).then_some((text, "codex:worked")); + } if row.starts_with(' ') { indented += 1; continue; @@ -618,6 +645,17 @@ fn codex_status(rows: &[String], composer: usize) -> Option<(String, &'static st None } +/// ` Worked for 1s • 4:19 PM` → `Worked for 1s`: the completion row codex +/// 0.162.1 paints under the last reply, indented like an attachment. Require +/// the ` • ` before the clock time: `Worked for` alone is conversation prose. +fn codex_worked(row: &str) -> Option { + let (duration, _) = row + .trim_start() + .strip_prefix("Worked for ")? + .split_once(" • ")?; + (!duration.is_empty()).then(|| format!("Worked for {duration}")) +} + /// Split a live status row into its header and the text after the opening /// parenthesis. The optional activity prefix is `• ` or `◦ `; the header must /// begin alphanumeric. [`codex_interrupt_paren`] supplies the fixed structure diff --git a/src/harness/summary_tests.rs b/src/harness/summary_tests.rs index 4439159..51188e3 100644 --- a/src/harness/summary_tests.rs +++ b/src/harness/summary_tests.rs @@ -692,19 +692,16 @@ fn codex_label_separates_right_aligned_warning_badges() { "⚠ 1 warning · /warnings to view", "⚠ 1 · f2", "⚠ 2 · shift+←", - "⚠ 3 · ⌥+w", "⚠ 4 · alt+w", "⚠ 5 · ctrl+x ctrl+w", "⚠ 6 · /warnings", - "⚠ 1 warning · ctrl+shift+⌥+f24 to view", - // Since Codex 0.160.0, `⌥` is displayed without `+` before the key. + // `⌥` is glued to its key. "⚠ 3 · ⌥w", "⚠ 2 warnings · ⌥w to view", "⚠ 1 warning · ctrl+shift+⌥f24 to view", "⚠ 5 · ctrl+x ⌥w", - // The `+` key itself, in the 0.160 and 0.159 spellings. + // The `+` key itself. "⚠ 1 · ⌥+", - "⚠ 1 · ⌥++", "⚠ 1 · fwd del", "⚠ 1 · ctrl+x ctrl+fwd del", "⚠ 1", @@ -772,40 +769,70 @@ fn codex_label_rejects_warning_prose_and_incomplete_footers() { assert_eq!(CodexSummary.model_label(&rows), None); } -/// Normalize braille frames to the bare title and mark them working. Canonicalize the -/// blocked-on-user blink to one phase; pass idle and foreign titles through. +/// The 0.162.1 title grammar is `[frame ]summary | cwd`: the summary is kept, +/// the cwd dropped, a frame marks working, and every frame renders one string. +/// The bare cwd and the frame placeholder yield nothing; the blocked blink +/// folds to one phase. #[test] -fn codex_title_animations_canonicalize_to_constant_text() { +fn codex_title_grammar_yields_the_summary() { + let t = |title: &str| CodexSummary.normalize_title(title); + let named = Some(("Count to twenty slowly".to_string(), true)); + let idle = Some(("Count to twenty slowly".to_string(), false)); + + // The four titles one turn shows, in order. + assert_eq!(t("codex-work"), None, "the cwd basename is not a summary"); + assert_eq!( + t("⠴ ⠴ | codex-work"), + None, + "the placeholder is a frame, not text" + ); + assert_eq!(t("⠙ Count to twenty slowly | codex-work"), named); + assert_eq!(t("Count to twenty slowly | codex-work"), idle); + for frame in BRAILLE_FRAMES { assert_eq!( - CodexSummary.normalize_title(&format!("{frame} fleetcom")), - Some(("fleetcom".to_string(), true)), + t(&format!("{frame} Count to twenty slowly | codex-work")), + named, + "{frame:?}" + ); + assert_eq!( + t(&format!("{frame} {frame} | codex-work")), + None, "{frame:?}" ); + assert_eq!( + t(&format!("{frame} fleetcom")), + Some(("fleetcom".to_string(), true)), + "{frame:?}: a frame-led title without a cwd is all summary" + ); } - // `[ . ]` folds to `[ ! ]`; `[ ! ]` already has the canonical text. + // The cwd is split off at the last separator; the summary keeps its own. assert_eq!( - CodexSummary.normalize_title("[ . ] Action Required | fleetcom"), - Some(("[ ! ] Action Required | fleetcom".to_string(), false)) - ); - assert_eq!( - CodexSummary.normalize_title("[ ! ] Action Required | fleetcom"), - None, - "the frozen phase needs no rewrite" + t("⠙ Fix a | b | codex-work"), + Some(("Fix a | b".to_string(), true)) ); - // Bare, foreign, frame-only, and empty titles do not match Codex's title - // grammar. - assert_eq!(CodexSummary.normalize_title("fleetcom"), None); - assert_eq!(CodexSummary.normalize_title("zellij: main"), None); - assert_eq!(CodexSummary.normalize_title("⠹"), None, "frame alone"); - assert_eq!( - CodexSummary.normalize_title("⠹ "), - None, - "frame with an empty rest" - ); - assert_eq!(CodexSummary.normalize_title(""), None, "empty title"); + // Both blink phases render the `[ ! ]` phase, with and without a cwd. + for title in [ + "[ . ] Action Required | fleetcom", + "[ ! ] Action Required | fleetcom", + "[ . ] Action Required", + ] { + assert_eq!( + t(title), + Some(("[ ! ] Action Required".to_string(), false)), + "{title:?}" + ); + } + + // Foreign, frame-only, empty-summary, and empty titles do not match. + assert_eq!(t("zellij: main"), None); + assert_eq!(t("⠹"), None, "frame alone"); + assert_eq!(t("⠹ "), None, "frame with an empty rest"); + assert_eq!(t(" | codex-work"), None, "empty summary"); + assert_eq!(t("⠹ | codex-work"), None, "empty summary behind a frame"); + assert_eq!(t(""), None, "empty title"); } /// The status row anchors on its parenthetical, not on a literal verb. The @@ -1038,6 +1065,53 @@ fn codex_ran_stops_at_foreign_rows() { assert_eq!(CodexSummary.live_preview(&behind_reply), None); } +/// The completion row `Worked for {duration} • {time}` is indented, so it is +/// read before the indentation skip and cut before the clock time. Without +/// the ` • ` it is prose and skipped like any attachment; behind a reply +/// bullet or beyond the indentation window it is stale. +#[test] +fn codex_worked_row_ends_the_turn() { + let tail = [ + "", + "› Ask Codex to do anything", + "", + " GPT-6-Luna max · 12.8K used · 20.7K in · 5 out", + ]; + let probe = |above: &[&str]| { + let mut rows = above.to_vec(); + rows.extend(tail); + CodexSummary.live_preview(&rs(&rows)) + }; + let worked = Some(("Worked for 1s".to_string(), "codex:worked")); + assert_eq!(probe(&["• ok", "", " Worked for 1s • 4:19 PM"]), worked); + assert_eq!( + probe(&[" Worked for 1m 05s • 4:19 PM"]), + Some(("Worked for 1m 05s".to_string(), "codex:worked")) + ); + for row in [ + " Worked for 1s", + " Worked for • 4:19 PM", + " Worked for 1s •4:19 PM", + ] { + assert_eq!(probe(&["• ok", "", row]), None, "{row:?}"); + } + assert_eq!( + probe(&[" Worked for 1s • 4:19 PM", "", "• ok"]), + None, + "a prior turn's completion row sits behind the reply bullet" + ); + + let mut deep = vec![" Worked for 1s • 4:19 PM"]; + deep.extend([" └ output"; 10]); + assert_eq!( + probe(&deep), + worked, + "ten indented rows are within the window" + ); + deep.push(" └ output"); + assert_eq!(probe(&deep), None, "eleven exhaust it"); +} + /// A hint row may follow the composer without a status line. The anchor /// still fires, without a model prefix. #[test] @@ -1378,162 +1452,96 @@ fn codex_0158_checkpoint(bytes: &[u8], offset: usize, alternate: bool) -> Vec_ OpenAI Codex (v0.162.1)` header with a same-width synthetic +working directory on the row below it; the `GPT-6-Luna max · … in · … out` +status line, token counters, clock times, and prompts are verbatim. The Claude fixtures with a model prefix open on claude 2.1.296's three-row logo header (`Claude Code v2.1.296`, `Fable 5.1 with high effort · Claude Max`, working directory); the working directory on the header row and on the statusline row below the input @@ -85,15 +90,18 @@ prevent a byte-for-byte match with the captured header. | `preview_claude_body_menu_idle.bin` | body from 2.1.215, header transplanted from 2.1.296: approval-menu text touching the chrome window, input box intact | extraction aborted at a foreign column-0 row; marker fallback | | `preview_claude_tasklist.bin` | 2.1.215, not re-observed on 2.1.296: spinner above a six-row task-list block, agent roster below the box | `claude:spinner` through a bounded indented block | | `preview_claude_body_above_tasklist.bin` | 2.1.215, not re-observed on 2.1.296: spinner-shaped body row above `⏺` prose and the task list | negative: status structure rejected on column-0 prose | -| `preview_codex_working.bin` | codex `• Working (7s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close` | `codex:working` normalization: affordances stripped, slow suffix kept | -| `preview_codex_working_over_ran.bin` | codex working with a `• Ran` row higher in the same turn | `codex:working` preferred at the pin; stale row excluded | -| `preview_codex_scrollback.bin` | codex finished turn, `• Ran` from the prior turn in scrollback | scan stopped at the reply bullet; floor-tier fallback | -| `preview_codex_ran.bin` | codex transient completion row | `codex:ran` extraction through the `└` attachment row | -| `preview_codex_hint_row.bin` | codex working with `tab to queue message` below the composer, no status line | `codex:working` through the composer pin; no model prefix without one | -| `preview_codex_approval.bin` | codex approval modal: composer and status line replaced by a numbered menu | `awaiting approval` synthesized by `codex:approval-menu` | -| `preview_codex_reasoning.bin` | codex status row with a reasoning phrase over an `• Explored` group and reply bullet; composer carrying text; no status line | header kept verbatim by `codex:working`; preceding `•`-headed rows excluded; no model prefix without a status line | -| `preview_codex_queued.bin` | codex `• Working` row separated from the composer by a `• Queued follow-up inputs` block; `model-with-reasoning · current-dir` status line below | `codex:working` extracted across queued-message heads; model label from the first status-line item | -| `preview_codex_body_menu.bin` | modal-shaped menu quoted in the body, live composer below | negative: modal match suppressed with composer present; floor-tier fallback | +| `preview_codex_working.bin` | codex 0.162.1, captured 2026-10-10: `• Working (3s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close` | `codex:working` normalization: affordances stripped, slow suffix kept | +| `preview_codex_working_over_ran.bin` | codex 0.144.6: working with a `• Ran` row higher in the same turn | `codex:working` preferred at the pin; stale row excluded | +| `preview_codex_scrollback.bin` | codex 0.144.6: finished turn, `• Ran` from the prior turn in scrollback | scan stopped at the reply bullet; floor-tier fallback | +| `preview_codex_ran.bin` | codex 0.144.6, synthetic: transient completion row (no raw capture holds it) | `codex:ran` extraction through the `└` attachment row | +| `preview_codex_ran_done.bin` | codex 0.162.1, captured 2026-10-10: finished turn, `• Ran curl -sI https://example.com` and its `└` output behind the `• HTTP/2 200` reply bullet | scan stopped at the reply bullet; marker fallback on the alternate screen | +| `preview_codex_queued.bin` | codex 0.162.1, captured 2026-10-10: `• Working` row separated from the composer by a `• Queued follow-up inputs` block with one `↳` message and its `⇧←` hint | `codex:working` extracted across queued-message heads; model label from the `in · out` status line | +| `preview_codex_done.bin` | codex 0.162.1, captured 2026-10-10: finished turn, `• ok` reply over the indented `Worked for 1s • 4:19 PM` completion row | `codex:worked` cut before the clock time | +| `preview_codex_idle.bin` | codex 0.162.1, captured 2026-10-10: launch state, command list over the `› Ask Codex to do anything` placeholder, `0 in · 0 out` status line | fall-through to the marker | +| `preview_codex_hint_row.bin` | codex 0.144.6, paste-derived from a 2026-07-20 sighting: working with `tab to queue message` below the composer, no status line | `codex:working` through the composer pin; no model prefix without one | +| `preview_codex_approval.bin` | codex 0.144.6, paste-derived from a 2026-07-20 sighting: approval modal, composer and status line replaced by a numbered menu | `awaiting approval` synthesized by `codex:approval-menu` | +| `preview_codex_reasoning.bin` | codex 0.147.0: status row with a reasoning phrase over an `• Explored` group and reply bullet; composer carrying text; no status line | header kept verbatim by `codex:working`; preceding `•`-headed rows excluded; no model prefix without a status line | +| `preview_codex_body_menu.bin` | codex 0.144.6, synthetic: modal-shaped menu quoted in the body, live composer below | negative: modal match suppressed with composer present; floor-tier fallback | | `preview_claude_waiting.bin` | 2.1.215, not re-observed on 2.1.296, where the `✻ Waiting for 1 background agent to finish` row was seen only in the body after the turn: waiting on a backgrounded subagent, `⏺` prose and agent roster around the box | ellipsis-less row extracted verbatim by `claude:waiting`; no model label mid-session | | `preview_claude_workflow_wait.bin` | 2.1.215, not re-observed on 2.1.296: waiting on a dynamic workflow, with 19 blank rows before the input box and a workflow roster below it | blank rows skipped during `claude:waiting` matching; the roster is excluded | | `preview_grok_working.bin` | grok braille spinner with elapsed/throughput ticker | `grok:spinner` cut at the label's `…`; border label read | @@ -115,16 +123,19 @@ prevent a byte-for-byte match with the captured header. | `preview_trunc_grok.bin` | synthetic 40×80: spinner label truncated with the CLI's ellipsis | CLI's own `…` kept verbatim | | `preview_wrap_grok.bin` | synthetic 40×30: ellipsis wrapped onto the next row | structure rejected; marker fallback | -## Codex 0.157.1 and 0.158.0 captures +## Codex 0.158.0 captures The versioned directories contain raw PTY streams from the official macOS arm64 -release executables, recorded on 2026-09-28. Codex ran with isolated homes, -`--no-daemon`, and a loopback Responses server supplying scripted replies: +0.158.0 release executable, recorded on 2026-09-28. Codex ran with isolated +homes, `--no-daemon`, and a loopback Responses server supplying scripted +replies. The 0.157.1 streams recorded alongside them were deleted on +2026-10-10: no matcher arm depended on them alone, and 0.158.0 itself is +behind the current release, 0.162.1. - [`codex_0158_preview`](codex_0158_preview/README.md) covers working status, queued input, Shift-Left draft retrieval, completed-turn rejection by the - working matcher, ordinary command approval, and the additional terminal-input - approval in 0.158.0. The existing approval matcher recognizes both menus. + working matcher, ordinary command approval, and the terminal-input approval + menu. The existing approval matcher recognizes both menus. - [`codex_0158_terminal`](codex_0158_terminal/README.md) covers direct Codex output across resize, `/new`, alternate-screen teardown, and OSC 52 copy. Separate outer fleetcom captures show resumed history and the exact clipboard diff --git a/tests/corpus/codex_0158_preview/0.157.1-fullscreen-approval.bin b/tests/corpus/codex_0158_preview/0.157.1-fullscreen-approval.bin deleted file mode 100644 index 593ea45..0000000 --- a/tests/corpus/codex_0158_preview/0.157.1-fullscreen-approval.bin +++ /dev/null @@ -1 +0,0 @@ -[?2004h[?1004h]10;?\]11;?\[>c[?2026h[?25l[?1049h[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[?2026h[?25l╭───────────────────────────────────────╮│ >_ OpenAI Codex (v0.157.1) ││ ││ model: loading /model to change ││ directory: loading │╰───────────────────────────────────────╯›Ask Codex to do anything?forshortcuts[0 q╭[?25h[?2026l[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l/private/tmp/…/workspace[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;workspace[?2026h[?25lTip:Run/reviewtogetacodereviewofyourcurrentchanges.phase0-fixture-model default[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l─────────────╮ │ │phase0-fixture-model /model to change │ │─────────────╯[?25h[?2026l[?2026h[?25lPhase zero fixture: replywiththecompletionmarker.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l› Phasezerofixture:replywiththecompletionmarker.Ask Codex to do anything?forshortcuts[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25l•Working(0s • esc to interrupt)⚠1 warning·f2toview[?25h[?2026l]0;⠙ ⠙ | workspace[?2026h[?25h[?2026l]0;⠹ ⠹ | workspace[?2026h[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25l◦[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l1[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l2[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦3[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l4[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l5[?25h[?2026l]0;[ ! ] Action Required | workspace[?2026h[?25l•Runningprintf 'PHASE0 APPROVAL\n'Would you like to run the following command?Environment:localReason:Phase zero approval fixture: print a marker.$printf 'PHASE0 APPROVAL\n' › 1. Yes, proceed (y)  2. Yes, and don't ask againforcommandsthatstartwith`printf"PHASE0APPROVAL\\n"`(p)3.No,andtellCodexwhattododifferently(esc)Press enter to confirm or esc to cancel [?2026l[?2026h[?2026l]0;[ . ] Action Required | workspace]0;[ ! ] Action Required | workspace]0;[ . ] Action Required | workspace]0;⠙ workspace[?2026h✗ You canceled the request to run printf 'PHASE0 APPROVAL\n'•Failed (exit 1)printf 'PHASE0 APPROVAL\n' └ (no output)•Working(5s • esc to interrupt) ›Ask Codex to do anythingphase0-fixture-model default? for shortcuts ⚠ 1 warning · f2 to view[?25h[?2026l]0;workspace[?2026h[?25l■ Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report theissue. Tip:Run/reviewtogetacodereviewofyourcurrentchanges.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l›Shutting down...[?2026l]0;[?1006l[?1015l[?1003l[?1002l[?1000l[?1007l[?1049l[<1u[?2004l[?1004l[0 q[?25h[0 q[?25h^C \ No newline at end of file diff --git a/tests/corpus/codex_0158_preview/0.157.1-fullscreen-stdin.bin b/tests/corpus/codex_0158_preview/0.157.1-fullscreen-stdin.bin deleted file mode 100644 index 5909ffa..0000000 --- a/tests/corpus/codex_0158_preview/0.157.1-fullscreen-stdin.bin +++ /dev/null @@ -1 +0,0 @@ -[?2004h[?1004h]10;?\]11;?\[>c[?2026h[?25l[?1049h[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[?2026h[?25l╭───────────────────────────────────────╮│ >_ OpenAI Codex (v0.157.1) ││ ││ model: loading /model to change ││ directory: loading │╰───────────────────────────────────────╯›Ask Codex to do anything?forshortcuts[0 q╭[?25h[?2026l[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l───────────────────╮ │ │ │/private/…/0.157.1-fullscreen-stdin/workspace │───────────────────╯[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;workspace[?2026h[?25lTip:Use/keymaptoconfigurekeyboardshortcuts.phase0-fixture-model default[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25lphase0-fixture-model /modeltochange[?25h[?2026l[?2026h[?25lPhase zero fixture: replywiththecompletionmarker.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l› Phasezerofixture:replywiththecompletionmarker.Ask Codex to do anything?forshortcuts[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l⚠1 warning·f2toview[?25h[?2026l]0;⠙ workspace[?2026h[?25l•Working(0s • esc to interrupt)[?25h[?2026l]0;⠙ ⠙ | workspace[?2026h[?25h[?2026l]0;⠹ ⠹ | workspace[?2026h[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l1[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l2[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦3[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ workspace[?2026h[?25l4[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;[ ! ] Action Required | workspace[?2026h[?25l•Runningread line; printf "PHASE0 INPUT: %s\n" "$line"Would you like to run the following command?Environment:localReason:Phase zero input approval fixture: read and echo one line.$read line; printf "PHASE0 INPUT: %s\n" "$line" › 1. Yes, proceed (y)  2. Yes, and don't ask againforcommandsthatstartwith`readline;printf"PHASE0INPUT:%s\n""$line"`(p)3.No,andtellCodexwhattododifferently(esc)Press enter to confirm or esc to cancel [?2026l[?2026h[?2026l]0;[ . ] Action Required | workspace]0;[ ! ] Action Required | workspace]0;⠙ workspace[?2026h✔ You approved codex to run read line; printf "PHASE0 INPUT: %s\n" "$line" this time•Runningread line; printf "PHASE0 INPUT: %s\n" "$line"•Working(5s • esc to interrupt) ›Ask Codex to do anythingphase0-fixture-model default? for shortcuts ⚠ 1 warning · f2 to view[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l6[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l └ fixture PHASE0 INPUT: fixture[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•a read line; printf "PHASE0 INPUT: %s\n" "$line"[?25h[?2026l]0;workspace[?2026h[?25l• PHASE0COMPLETE.Deterministiclocalresponse. 2:42 PM  Tip:Switchmodelsorreasoningeffortquicklywith/model.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25lesc again o editpreviousmessage[?25h[?2026l[?2026h[?25lTip:Switchmodelsorreasoningeffortquicklywith/model.? for shorcuts   ⚠1 warning·f2toview[?25h[?2026l[?2026h[?25l›Shutting down...[?2026l]0;[?1006l[?1015l[?1003l[?1002l[?1000l[?1007l[?1049l[<1u[?2004l[?1004l[0 q[?25h[0 q[?25h^C \ No newline at end of file diff --git a/tests/corpus/codex_0158_preview/0.157.1-inline.bin b/tests/corpus/codex_0158_preview/0.157.1-inline.bin deleted file mode 100644 index d13337c..0000000 --- a/tests/corpus/codex_0158_preview/0.157.1-inline.bin +++ /dev/null @@ -1,17 +0,0 @@ -[?2004h[?1004h]10;?\]11;?\[>c[?2026h[?2026h[?25l╭───────────────────────────────────────╮│ >_ OpenAI Codex (v0.157.1) ││ ││ model: loading /model to change ││ directory: loading │╰───────────────────────────────────────╯  › Ask Codex to do anything  [0 q╭[?25h[?2026l[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l─────────────╮ │ │ │/private/tmp/…/0.157.1-inline/workspace │─────────────╯[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;workspace[?2026h[?25lphase0-fixture-model default[?25h[?2026l[?2026h[?25h[?2026l[?2026hMMMMMMMM -╭────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.157.1) │ -│ │ -│ model: phase0-fixture-model /model to change │ -│ directory: /private/tmp/…/0.157.1-inline/workspace │ -╰────────────────────────────────────────────────────╯ - - Tip: New Build faster with the Desktop app. Run 'codex app' or visit ]8;;https://chatgpt.com/codex?app-landing-page=truehttps://chatgpt.com/codex?app-landing-page=true]8;;[?25l  › Ask Codex to do anything  phase0-fixture-model default[0 q [?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25lPhase zero fixture: replywiththecompletionmarker.[?25h[?2026l[?2026hMMMM - - -› Phase zero fixture: reply with the completion marker. -[?25lAsk Codex to do anything[0 q [?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l⚠1 warning·f2toview[?25h[?2026l]0;⠙ workspace[?2026h[?25l • Working (0s • esc to interrupt)  › Ask Codex to do anything  phase0-fixture-model default ⚠ 1 warning · f2 to view[0 q [?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l]0;⠹ ⠹ | workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25l◦[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ workspace[?2026h[?25l1[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25l◦[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ workspace[?2026h[?25l2[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ workspace[?2026h[?25l◦3[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l4[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25l◦[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25lQueued phase zero messagetab to queue message   100% contex left[?25h[?2026l]0;⠹ workspace[?2026h[?25l5[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25l◦[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l ◦ Working (5s • esc to interrupt) • Queued follow-up inputs ↳ Queued phase zero message ⌥+↑ edit last queued message › Ask Codex to do anything  phase0-fixture-model default ⚠ 1 warning · f2 to view[0 q [?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ workspace[?2026h[?25l•6[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l • Working (6s • esc to interrupt)  › Queued phase zero message  tab to queue message 100% context left[0 q [?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25l◦[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ workspace[?2026h[?25l7[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25lAsk Codex to do anythingphase0-fixture-model default⚠1 warning · f2 o view[?25h[?2026l[?2026hMM - -• PHASE0 COMPLETE. Deterministic local response.[?25l  › Ask Codex to do anything  phase0-fixture-model default ⚠ 1 warning · f2 to view[0 q [?25h[?2026l]0;workspace[?2026hMM - - 2:42 PM[?25l[0 q [?25h[?2026l[?2026h[?25l  › Shutting down... [?2026l]0;[<1u[?2004l[?1004l[0 q[?25h[0 q[?25h^C \ No newline at end of file diff --git a/tests/corpus/codex_0158_preview/README.md b/tests/corpus/codex_0158_preview/README.md index b82a485..c91382f 100644 --- a/tests/corpus/codex_0158_preview/README.md +++ b/tests/corpus/codex_0158_preview/README.md @@ -1,11 +1,12 @@ -# Codex 0.157.1 and 0.158.0 preview captures +# Codex 0.158.0 preview captures -Captured 2026-09-28 from the official macOS arm64 release executables in a +Captured 2026-09-28 from the official macOS arm64 release executable in a 40-row × 120-column PTY with `TERM=xterm-256color`. Each file is the complete, unmodified direct Codex PTY stream from startup through teardown. Tests replay prefixes at the byte checkpoints below. There are no resize events in these -six scenarios. The `fullscreen` scenarios use Codex's alternate screen; they -are not captures of an outer fleetcom client. +three scenarios. The `fullscreen` scenarios use Codex's alternate screen; they +are not captures of an outer fleetcom client. The 0.157.1 pair of each +scenario, captured the same day, was deleted on 2026-10-10. The Responses endpoint was a loopback server with scripted replies and harmless tool calls. Each run used an isolated home and workspace, `--no-daemon`, and @@ -17,19 +18,16 @@ behavior. Approval tests also verify preview resolution. The inline scenario submits a prompt, queues `Queued phase zero message` with Tab, retrieves it with Shift-Left (`ESC [ 1 ; 2 D`), clears the draft with -Ctrl-U, and waits for completion. Both releases retrieve the draft, although -0.157.1 displays the older Option-Up hint. The approval scenario declines a -`printf` command. The stdin scenario approves a shell command that reads one -line, then the mock requests `fixture\n` as terminal input. 0.157.1 completes; -0.158.0 displays a second approval menu. Tests check the rendered distinctions -and adapter results against the reviewed capture screens, not generated -expectations. +Ctrl-U, and waits for completion. The approval scenario declines a `printf` +command. The stdin scenario approves a shell command that reads one line, then +the mock requests `fixture\n` as terminal input and a second approval menu is +displayed. Tests check the rendered rows and adapter results against the +reviewed capture screens, not generated expectations. ## Producer provenance | Version | Source commit | Official macOS arm64 tarball SHA-256 | | --- | --- | --- | -| 0.157.1 | `36650394c5b38c2990ccf2a3457165ca3e9d9726` | `3c45b162b7a76f51325015b1d0a8112c73219b7a9b59cd5762c37c9ba55894fa` | | 0.158.0 | `064c6b8c737f5b41d171fdda80bd9ef10ad06eb3` | `341c4a08f9ce1935b3007376dc2a3d50a0a89112930e9a474ae61367218f6e8a` | Imported from the matching scenario's `session.bin` in the Phase 0 evidence @@ -45,11 +43,8 @@ Offsets are exclusive byte ends measured from startup. Dimensions remain | Stream | Tested checkpoints | Bytes | SHA-256 | | --- | --- | ---: | --- | -| `0.157.1-inline.bin` | working 12405; queued 15613; Shift-Left 17650; completed 18716 | 18979 | `3948ac17127d2a8b87e52ce33c21ae5fd481561138534d773e9c857eeedd5a93` | | `0.158.0-inline.bin` | working 11311; queued 14478; Shift-Left 16558; completed 17253 | 17516 | `891a15ed16251b0af90ca3cf06311d401fef8e7a0bf93a3fea240f3a2f74f8ac` | -| `0.157.1-fullscreen-approval.bin` | approval 11965 | 13572 | `cbc65ae4387235d0b6a7d724984125d72bd7ed0348e39f41866b5426783ba99a` | | `0.158.0-fullscreen-approval.bin` | approval 12967 | 14592 | `357b3c8c5b4d6b34b422d08f745d92700b4969635d8e29282d1cb76b48b84d8e` | -| `0.157.1-fullscreen-stdin.bin` | command approval 12494; completed after stdin 16518 | 17327 | `07e950cd24ae9a93684ca427714eb6a9507a64a08987c2de6263476d96bc4efe` | | `0.158.0-fullscreen-stdin.bin` | command approval 13496; stdin approval 17417 | 19416 | `cb8c3662a0121b8dd1fd73ee3d15de3ae9208bdd72f054b1c71c58460d27ab47` | These captures do not establish Linux CLI rendering, real-account behavior, diff --git a/tests/corpus/codex_0158_terminal/0.157.1-fullscreen-fleetcom-load.bin b/tests/corpus/codex_0158_terminal/0.157.1-fullscreen-fleetcom-load.bin deleted file mode 100644 index ff11837..0000000 --- a/tests/corpus/codex_0158_terminal/0.157.1-fullscreen-fleetcom-load.bin +++ /dev/null @@ -1 +0,0 @@ -[?u[?1049h[?25l[?2004h[?1007s[?1007h[?1004s[?1004h[?2026h[?25l fleetcom 0 running · 0 idle · 0 done by state · dir · custom · foreground                                       ❯ n run · @ dir · / find · s sort  ↑↓ select · enter attach · space peek · ? controls [?25l[?2026l[?2026h[?25l fleetcom 1 running · 0 idle · 0 done by state · dir · custom · foreground   Running  ✻ codex resume '01a0e9fa-42… 0s                                   loaded 'phase0-saved': 1 task(s)  ↑↓ select · enter attach · space peek · ? controls [?25l[?2026l[?2026h[?25l fleetcom 1 running · 0 idle · 0 done by state · dir · custom · foreground   Running  ✻ codex resume '01a0e9fa-42… full-screen 0s                                   loaded 'phase0-saved': 1 task(s)  ↑↓ select · enter attach · space peek · ? controls [?25l[?2026l[?2026h[?25l fleetcom 1 running · 0 idle · 0 done by state · dir · custom · foreground   Running  ✻ codex resume '01a0e9fa-42… workspace 0s                                   loaded 'phase0-saved': 1 task(s)  ↑↓ select · enter attach · space peek · ? controls [?25l[?2026l[?2026h[?25l fleetcom 1 running · 0 idle · 0 done by state · dir · custom · foreground   Running  ✻ codex resume '01a0e9fa-42… workspace 1s                                   loaded 'phase0-saved': 1 task(s)  ↑↓ select · enter attach · space peek · ? controls [?25l[?2026l[?2026h[?25l fleetcom 1 running · 0 idle · 0 done by state · dir · custom · foreground   Running  ✻ codex resume '01a0e9fa-42… workspace 2s                                   loaded 'phase0-saved': 1 task(s)  ↑↓ select · enter attach · space peek · ? controls [?25l[?2026l[?2026h[?25l fleetcom 1 running · 0 idle · 0 done by state · dir · custom · foreground   Running  ✻ codex resume '01a0e9fa-42… workspace 3s                                   loaded 'phase0-saved': 1 task(s)  ↑↓ select · enter attach · space peek · ? controls [?25l[?2026l[?2026h[?25l fleetcom 1 running · 0 idle · 0 done by state · dir · custom · foreground   Running  ✻ codex resume '01a0e9fa-42… workspace 4s                                   loaded 'phase0-saved': 1 task(s)  ↑↓ select · enter attach · space peek · ? controls [?25l[?2026l[?2026h[?25l [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25l[?2026l[?1000h[?1002h[?1003h[?1015h[?1006h[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM                     Tip: Use /feedback to send logs to the maintainers when something looks off.  › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM                      › Phase zero fixture: reply with the completion marker.   phase0-fixture-model default  [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                • Working (0s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                ◦ Working (0s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                ◦ Working (1s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                • Working (1s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                ◦ Working (1s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                ◦ Working (2s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                • Working (2s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                ◦ Working (3s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                • Working (3s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                • Working (4s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                ◦ Working (4s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                • Working (4s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                • Working (4s • esc to interrupt)   › Queued phase zero message   phase0-fixture-model default  tab to queue message [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                • Working (5s • esc to interrupt)   › Queued phase zero message   phase0-fixture-model default  tab to queue message [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                ◦ Working (5s • esc to interrupt)   › Queued phase zero message   phase0-fixture-model default  tab to queue message [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.            ◦ Working (5s • esc to interrupt)  • Queued follow-up inputs  ↳ Queued phase zero message  ⌥+↑ edit last queued message   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.            • Working (6s • esc to interrupt)  • Queued follow-up inputs  ↳ Queued phase zero message  ⌥+↑ edit last queued message   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                • Working (6s • esc to interrupt)   › Queued phase zero message   phase0-fixture-model default  tab to queue message [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                ◦ Working (6s • esc to interrupt)   › Queued phase zero message   phase0-fixture-model default  tab to queue message [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                ◦ Working (7s • esc to interrupt)   › Queued phase zero message   phase0-fixture-model default  tab to queue message [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                • Working (7s • esc to interrupt)   › Queued phase zero message   phase0-fixture-model default  tab to queue message [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.                • Working (7s • esc to interrupt)   › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.                › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:46 PM             Tip: Use /theme to choose a syntax highlighting theme.  › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:46 PM              › Ask Codex to do anything   phase0-fixture-model default  ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:46 PM            › /copy copy the last response or part of it  › /copy   phase0-fixture-model default  [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:46 PM             Copy to clipboard   › 1. Whole response PHASE0 COMPLETE. Deterministic local response.   enter select · esc back  [?25l [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' Ctrl-\ background [?25l[?2026l]52;c;UEhBU0UwIENPTVBMRVRFLiBEZXRlcm1pbmlzdGljIGxvY2FsIHJlc3BvbnNlLg==[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:46 PM             Tip: Use /theme to choose a syntax highlighting theme.  › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' copied 46 chars [?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.157.1) │ │ │ │ model: phase0-fixture-model /model to change │ │ directory: /private/tmp/…/workspace │ ╰────────────────────────────────────────────────────╯   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:45 PM   › Phase zero fixture: reply with the completion marker.   • PHASE0 COMPLETE. Deterministic local response.   2:46 PM  • Copied Whole response to clipboard           Tip: Use /theme to choose a syntax highlighting theme.  › Ask Codex to do anything   phase0-fixture-model default  ? for shortcuts ⚠ 1 warning · f2 to view [?25h [attached] codex resume '01a0e9fa-426f-7162-a215-3cea7cd6e6dd' copied 46 chars [?25h[?2026l \ No newline at end of file diff --git a/tests/corpus/codex_0158_terminal/0.157.1-fullscreen-transitions.bin b/tests/corpus/codex_0158_terminal/0.157.1-fullscreen-transitions.bin deleted file mode 100644 index b1f52ba..0000000 --- a/tests/corpus/codex_0158_terminal/0.157.1-fullscreen-transitions.bin +++ /dev/null @@ -1 +0,0 @@ -[?2004h[?1004h]10;?\]11;?\[>c[?2026h[?25l[?1049h[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[?2026h[?25l╭───────────────────────────────────────╮│ >_ OpenAI Codex (v0.157.1) ││ ││ model: loading /model to change ││ directory: loading │╰───────────────────────────────────────╯›Ask Codex to do anything?forshortcuts[0 q╭[?25h[?2026l[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l/private/tmp/…/workspace[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;workspace[?2026h[?25lTip:Youcanresumeapreviousconversationbyrunningcodex resumephase0-fixture-model default[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l─────────────╮ │ │phase0-fixture-model /model to change │ │─────────────╯[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25lPhase zero fixture: replywiththecompletionmarker.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l› Phasezerofixture:replywiththecompletionmarker.Ask Codex to do anything?forshortcuts[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l⚠1 warning·f2toview[?25h[?2026l]0;⠙ workspace[?2026h[?25l•Working(0s • esc to interrupt)[?25h[?2026l]0;⠙ ⠙ | workspace[?2026h[?25h[?2026l]0;⠹ ⠹ | workspace[?2026h[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l1[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l2[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦3[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l4[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25l◦[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l5Queued phase zero messagetab to queue message[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦Working(5s • esc to interrupt)• Queuedfollow-upinputs ↳ Queued phase zero message  ⌥+↑ edit last queued mssageAsk Codex to do anything? for shortcuts ⚠1 warning·f2toview[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•6[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l   •Working (6s • esc to intrrupt)Queued phase zero messagetab to queue message[?25h[?2026l]0;⠧ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l◦[?25h[?2026l]0;⠇ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l7[?25h[?2026l]0;⠹ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠼ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25lAsk Codex to do anything? for shortcuts ⚠1 warning·f2toview[?25h[?2026l[?2026h[?25l• PHASE0COMPLETE.Deterministiclocalresponse. [?25h[?2026l]0;workspace[?2026h[?25l 2:42 PM Tip:Pressctrl+gtoedityourcurrentdraftinanexternaleditor.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l   [?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l› /copy copy the last response or part of it /copy[?25h[?2026l[?2026h[?25l Copy to clipboard › 1. Whole response PHASE0 COMPLETE. Deterministic local response. enter select · esc back [?2026l[?2026h[?2026l]52;c;UEhBU0UwIENPTVBMRVRFLiBEZXRlcm1pbmlzdGljIGxvY2FsIHJlc3BvbnNlLg==[?2026h Tip:Pressctrl+gtoedityourcurrentdraftinanexternaleditor.›Ask Codex to do anything phase0-fixture-model default? for shortcuts ⚠ 1 warning · f2 to view[?25h[?2026l[?2026h[?25l• CopiedWholeresponsetoclipboard[?25h[?2026l[?2026h[?25l│ ││ model: phase0-fixture-model /model to change ││ directory: /private/tmp/…/workspace │╰────────────────────────────────────────────────────╯› Phasezerofixture:replywiththecompletionmarker.• PHASE0COMPLETE.Deterministiclocalresponse. 2:42 PM • CopiedWholeresponsetoclipboardTip:Pressctrl+rtosearchpreviouslyenteredprompts.›Ask Codex to do anythingphase0-fixture-model default?forshortcuts⚠1 warning·f2toview[0 q│[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l╭────────────────────────────────────────────────────╮│ >_ OpenAI Codex (v0.157.1) ││ ││ model: phase0-fixture-model /model to change ││ directory: /private/tmp/…/workspace │╰────────────────────────────────────────────────────╯› Phasezerofixture:replywiththecompletionmarker.• PHASE0COMPLETE.Deterministiclocalresponse. 2:42 PM • CopiedWholeresponsetoclipboardTip:Pressctrl+gtoedityourcurrentdraftinanexternaleditor.›Ask Codex to do anythingphase0-fixture-model default?forshortcuts⚠1 warning·f2toview[0 q╭[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l   [?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l› /new start a new chat during a conversation /new[?25h[?2026l[?2026h[?25l Tip: Press ctrl+g to edit your current draft in an external editor.Ask Codex to do anything?forshortcuts⚠1 warning·f2toview[?25h[?2026l]0;workspace[?2026h[?25l╭────────────────────────────────────────────────────╮│ >_ OpenAI Codex (v0.157.1) ││ ││ model: phase0-fixture-model /model to change ││ directory: /private/tmp/…/workspace │╰────────────────────────────────────────────────────╯Tokenusage:total=20input=10output=10Tocontinuethissession,runcodex resume 01a0e9f7-8d12-76b1-80a8-a50519150e72Tip:Youcanresumeapreviousconversationbyrunningcodex resume›Ask Codex to do anythingphase0-fixture-model default?forshortcuts[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l›Shutting down...[?2026l]0;[?1006l[?1015l[?1003l[?1002l[?1000l[?1007l[?1049l[<1u[?2004l[?1004l[0 q[?25h[0 q[?25h^C \ No newline at end of file diff --git a/tests/corpus/codex_0158_terminal/README.md b/tests/corpus/codex_0158_terminal/README.md index 5dc88a4..f83050d 100644 --- a/tests/corpus/codex_0158_terminal/README.md +++ b/tests/corpus/codex_0158_terminal/README.md @@ -1,8 +1,9 @@ # Codex terminal release captures -Recorded 2026-09-28 with the official arm64 macOS Codex 0.157.1 and 0.158.0 -executables. `manifest.json` preserves source commits, executable/archive hashes, -capture fleetcom provenance, stream hashes, checkpoint offsets, and resize events. +Recorded 2026-09-28 with the official arm64 macOS Codex 0.158.0 executable; the +0.157.1 pair recorded alongside it was deleted on 2026-10-10. `manifest.json` +preserves the source commit, executable/archive hashes, capture fleetcom +provenance, stream hashes, checkpoint offsets, and resize events. The capture fleetcom tree was d138a752 plus the recorded dirty patch, not a clean release checkout. These are raw terminal bytes, not constructed repaint fixtures. diff --git a/tests/corpus/codex_0158_terminal/manifest.json b/tests/corpus/codex_0158_terminal/manifest.json index 981d6f2..b01a3a6 100644 --- a/tests/corpus/codex_0158_terminal/manifest.json +++ b/tests/corpus/codex_0158_terminal/manifest.json @@ -6,125 +6,15 @@ "dirty_patch_sha256": "a0e37cbc2c4877c058a3bcf295cde301086049ea6ed300b7b9244c3d0d15e182", "fleetcom_binary_sha256": "175904b97d627cdac27f1d062c04a99ff6c63e85f3801a98c5f6920da4a529c6", "versions": { - "0.157.1": { - "binary_sha256": "27ceb5f9b957b43a519efe4eaa3816a0bffb0a531a2c89af18840c0a3c016a7d", - "tarball_sha256": "3c45b162b7a76f51325015b1d0a8112c73219b7a9b59cd5762c37c9ba55894fa" - }, "0.158.0": { "binary_sha256": "788a818fbb9596869c7a487554507cb8bdca17584b8671112b23f9e225ba35c8", "tarball_sha256": "341c4a08f9ce1935b3007376dc2a3d50a0a89112930e9a474ae61367218f6e8a" } }, "source_commits": { - "0.157.1": "36650394c5b38c2990ccf2a3457165ca3e9d9726", "0.158.0": "064c6b8c737f5b41d171fdda80bd9ef10ad06eb3" }, "fixtures": [ - { - "name": "0.157.1-fullscreen-transitions", - "surface": "direct Codex", - "initial_rows": 40, - "initial_cols": 120, - "bytes": 23780, - "sha256": "6757c1fe0246248da21ce43cb8745f54661942f69359a5c1b26bab2688b18a60", - "source_session_sha256": "6757c1fe0246248da21ce43cb8745f54661942f69359a5c1b26bab2688b18a60", - "marks": [ - { - "name": "startup", - "offset": 2396, - "rows": 40, - "cols": 120 - }, - { - "name": "completed", - "offset": 16312, - "rows": 40, - "cols": 120 - }, - { - "name": "copy-menu", - "offset": 17320, - "rows": 40, - "cols": 120 - }, - { - "name": "copied", - "offset": 18119, - "rows": 40, - "cols": 120 - }, - { - "name": "narrow", - "offset": 19460, - "rows": 20, - "cols": 60 - }, - { - "name": "restored", - "offset": 21163, - "rows": 40, - "cols": 120 - }, - { - "name": "new-thread", - "offset": 23499, - "rows": 40, - "cols": 120 - }, - { - "name": "exit", - "offset": 23780, - "rows": 40, - "cols": 120 - } - ], - "resizes": [ - { - "offset": 18119, - "resize": [ - 20, - 60 - ] - }, - { - "offset": 19460, - "resize": [ - 40, - 120 - ] - } - ] - }, - { - "name": "0.157.1-fullscreen-fleetcom-load", - "surface": "outer fleetcom client", - "initial_rows": 40, - "initial_cols": 120, - "bytes": 227987, - "sha256": "1a72dbf52c4db51de4ca0e7b57dec5873eb29d82891a79243995a1911ec2c531", - "source_session_sha256": "045e070f0dd09b9427db1c23bd5b2f0d64a2abcaf80f73a9485776d6de53fcaa", - "marks": [ - { - "name": "attached", - "offset": 47465, - "rows": 40, - "cols": 120 - }, - { - "name": "completed", - "offset": 199735, - "rows": 40, - "cols": 120 - }, - { - "name": "copied", - "offset": 227987, - "rows": 40, - "cols": 120 - } - ], - "resizes": [] - }, { "name": "0.158.0-fullscreen-transitions", "surface": "direct Codex", diff --git a/tests/corpus/preview_codex_done.bin b/tests/corpus/preview_codex_done.bin new file mode 100644 index 0000000..8cf72fa --- /dev/null +++ b/tests/corpus/preview_codex_done.bin @@ -0,0 +1,40 @@ +[?1049h + >_ OpenAI Codex (v0.162.1) + /Users/fleet-pilot/Projects/fixture-sources/pty/codex-cli-0-162-1/…/scratchpad/codex-work + + To get started, describe a task or try one of these commands: + + /init - create an AGENTS.md file with instructions for Codex + /status - show current session configuration + /permissions - choose what Codex is allowed to do + /model - choose what model and reasoning effort to use + /review - review any changes and find issues + + +› Reply with exactly the word ok and nothing else. + + +• ok + + Worked for 1s • 4:19 PM + + + + + + + + + + + + + + + + + +› Ask Codex to do anything + + GPT-6-Luna max · 12.8K used · 20.7K in · 5 out + ← for agents · ? for shortcuts \ No newline at end of file diff --git a/tests/corpus/preview_codex_idle.bin b/tests/corpus/preview_codex_idle.bin new file mode 100644 index 0000000..0955fce --- /dev/null +++ b/tests/corpus/preview_codex_idle.bin @@ -0,0 +1,40 @@ +[?1049h + >_ OpenAI Codex (v0.162.1) + /Users/fleet-pilot/Projects/fixture-sources/pty/codex-cli-0-162-1/…/scratchpad/codex-work + + To get started, describe a task or try one of these commands: + + /init - create an AGENTS.md file with instructions for Codex + /status - show current session configuration + /permissions - choose what Codex is allowed to do + /model - choose what model and reasoning effort to use + /review - review any changes and find issues + + + + + + + + + + + + + + + + + + + + + + + + + +› Ask Codex to do anything + + GPT-6-Luna max · 0 in · 0 out + ← for agents · ? for shortcuts \ No newline at end of file diff --git a/tests/corpus/preview_codex_queued.bin b/tests/corpus/preview_codex_queued.bin index 99cbfb9..c13dda9 100644 --- a/tests/corpus/preview_codex_queued.bin +++ b/tests/corpus/preview_codex_queued.bin @@ -1,10 +1,20 @@ - +[?1049h + >_ OpenAI Codex (v0.162.1) + /Users/fleet-pilot/Projects/fixture-sources/pty/codex-cli-0-162-1/…/scratchpad/codex-work + To get started, describe a task or try one of these commands: + /init - create an AGENTS.md file with instructions for Codex + /status - show current session configuration + /permissions - choose what Codex is allowed to do + /model - choose what model and reasoning effort to use + /review - review any changes and find issues +› Run the shell command sleep 25 and then say done. +• I’ll run sleep 25 now. @@ -15,26 +25,16 @@ -• Working (0s • esc to interrupt) + + +• Working (10s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close • Queued follow-up inputs - ↳ Hello, world! 0 - ↳ Hello, world! 1 - ↳ Hello, world! 2 - ↳ Hello, world! 3 - ↳ Hello, world! 4 - ↳ Hello, world! 5 - ↳ Hello, world! 6 - ↳ Hello, world! 7 - ↳ Hello, world! 8 - ↳ Hello, world! 9 - ↳ Hello, world! 10 - ↳ Hello, world! 11 - ↳ Hello, world! 12 - ↳ Hello, world! 13 - ↳ Hello, world! 14 - ↳ Hello, world! 15 + ↳ Also tell me the current year. + ⇧← edit last queued message + › Ask Codex to do anything - gpt-5.6-sol default · /tmp/project \ No newline at end of file + GPT-6-Luna max · 0 in · 0 out + ← for agents · ? for shortcuts \ No newline at end of file diff --git a/tests/corpus/preview_codex_ran_done.bin b/tests/corpus/preview_codex_ran_done.bin new file mode 100644 index 0000000..1b5cdaa --- /dev/null +++ b/tests/corpus/preview_codex_ran_done.bin @@ -0,0 +1,40 @@ +[?1049h + >_ OpenAI Codex (v0.162.1) + /Users/fleet-pilot/Projects/fixture-sources/pty/codex-cli-0-162-1/…/scratchpad/codex-work + + To get started, describe a task or try one of these commands: + + /init - create an AGENTS.md file with instructions for Codex + /status - show current session configuration + /permissions - choose what Codex is allowed to do + /model - choose what model and reasoning effort to use + /review - review any changes and find issues + + +› Run the shell command curl -sI https://example.com and show me the first line of output. + + +• I’ll run that command and return its first output line. + +• Ran curl -sI https://example.com + └ cf-ray: 0000000000000000-XXX + alt-svc: h3=":443"; ma=86400 + + + 9 lines (⌃t to expand) + +• HTTP/2 200 + + + + + + + + + + + +› Ask Codex to do anything + + GPT-6-Luna max · 3.92K used · 41.2K in · 106 out + ← for agents · ? for shortcuts \ No newline at end of file diff --git a/tests/corpus/preview_codex_working.bin b/tests/corpus/preview_codex_working.bin index 99e061c..31c331a 100644 --- a/tests/corpus/preview_codex_working.bin +++ b/tests/corpus/preview_codex_working.bin @@ -1,34 +1,28 @@ -╭────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.144.6) │ -│ │ -│ model: gpt-5.6-sol high /model to change │ -│ directory: ~/Projects/fixture-sources │ -╰────────────────────────────────────────────────╯ +[?1049h + >_ OpenAI Codex (v0.162.1) + /Users/fleet-pilot/Projects/fixture-sources/pty/codex-cli-0-162-1/…/scratchpad/codex-work - Tip: New Use /fast to enable our fastest inference with increased plan usage. + To get started, describe a task or try one of these commands: -⚠ MCP client for `demo-mcp-unresolved` failed to start: MCP startup failed: No such file or directory (os error 2) + /init - create an AGENTS.md file with instructions for Codex + /status - show current session configuration + /permissions - choose what Codex is allowed to do + /model - choose what model and reasoning effort to use + /review - review any changes and find issues -• You have 3 usage limit resets available. Run /usage to use one. -⚠ MCP startup incomplete (failed: demo-mcp-unresolved) +› Run the shell command sleep 25 and then say done. -› run: sleep 5 && echo ok +• I’ll run sleep 25 now. -⚠ MCP client for `demo-mcp-unresolved` failed to start: MCP startup failed: No such file or directory (os error 2) -⚠ MCP startup incomplete (failed: demo-mcp-unresolved) -• I’m running that command now. -• Working (7s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close -› Write tests for @filename - gpt-5.6-sol high · 0 in · 0 out @@ -37,3 +31,10 @@ +• Working (3s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close + + +› Ask Codex to do anything + + GPT-6-Luna max · 0 in · 0 out + ← for agents · ? for shortcuts \ No newline at end of file From c469358ffad4d1d735f8002683492f205ecf8000 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 10 Oct 2026 17:13:18 -0700 Subject: [PATCH 4/5] Refresh grok support for 1.0.50 and retire the omp pre-18.3.2 notes: Tasks panel skipped, still-running arm deleted, fixtures added --- docs/agent-resume.md | 1 - src/harness/assets.rs | 34 ++--- src/harness/omp.rs | 2 - src/harness/summary.rs | 79 +++++----- src/harness/summary_tests.rs | 136 +++++++++--------- tests/corpus/README.md | 5 +- .../corpus/preview_grok_background_tasks.bin | 39 +++++ tests/corpus/preview_grok_still_running.bin | 39 ----- .../corpus/preview_grok_waiting_subagent.bin | 39 +++++ 9 files changed, 203 insertions(+), 171 deletions(-) create mode 100644 tests/corpus/preview_grok_background_tasks.bin delete mode 100644 tests/corpus/preview_grok_still_running.bin create mode 100644 tests/corpus/preview_grok_waiting_subagent.bin diff --git a/docs/agent-resume.md b/docs/agent-resume.md index 96ef1ad..3190895 100644 --- a/docs/agent-resume.md +++ b/docs/agent-resume.md @@ -129,7 +129,6 @@ At `agent_end`, after every turn, report two changes without dedicated events: s As a result: - For a fresh `omp` task, no ID is captured until the end of the first turn. If you save before then, no ID is stored, so you start fresh on reload. -- Before omp 18.3.2, `ctx.agent` is unavailable, so no session ID is captured: an `omp` task is always saved without an ID. - After switching or branching to a session without a file on disk, continue using the previous ID until the end of the next turn. Write each report to a temporary file beside the capture file, then rename it over the destination to avoid reads of partial payloads. Capture is best-effort: return immediately for an empty capture path and ignore write errors. diff --git a/src/harness/assets.rs b/src/harness/assets.rs index b89db3f..2fb1f1a 100644 --- a/src/harness/assets.rs +++ b/src/harness/assets.rs @@ -35,10 +35,10 @@ //! replace `$FLEETCOM_CAPTURE_FILE` with `{reason, sessionId, sessionFile, //! cwd}` JSON. Write `..tmp` beside the capture file and //! rename it over the destination to avoid reads of partial payloads. -//! Report only the top-level session (`ctx.agent.kind` is `"main"`) and -//! only when `sessionFile` exists on disk. On omp older than 18.3.2, -//! `ctx.agent` is absent, so write nothing. Skip unset or empty capture -//! paths and ignore all errors. +//! Report only the top-level session (`ctx.agent.kind` is `"main"`), since +//! sub-agents share the handlers and their IDs are not resumable, and only +//! when `sessionFile` exists on disk. Skip unset or empty capture paths and +//! ignore all errors. use std::{ fs, io, @@ -89,9 +89,8 @@ exec "$@" /// Report only an ID usable with `omp --resume`: /// /// - Only the top-level session's ID. The same handlers are registered for -/// sub-agent sessions, with the child's ID in their session manager. -/// - None on an omp older than 18.3.2. `ctx.agent` is absent there, so the -/// top-level session cannot be told from a sub-agent. +/// sub-agent sessions, with the child's ID in their session manager, and a +/// sub-agent's ID is not resumable. /// - Only after the session file exists on disk. For a fresh session, first /// report at `agent_end`. /// @@ -106,7 +105,7 @@ function write(ctx, reason) { const path = process.env.FLEETCOM_CAPTURE_FILE; if (!path) return; // These handlers are also registered for sub-agent sessions, which cannot - // be resumed by ID. Without `ctx.agent` (before 18.3.2), report nothing. + // be resumed by ID: only the top-level session reports. if (ctx.agent?.kind !== "main") return; // The session file is created after the first assistant message. Until // then, the ID cannot be found through `omp --resume`. @@ -350,9 +349,8 @@ mod tests { /// Stand-in for omp's extension host. Load the module, collect its /// registrations, and fire one event with a context built from the /// arguments ` `. An empty - /// kind means no `ctx.agent`, as before omp 18.3.2; an empty file means - /// `undefined` from `getSessionFile()`. Print the registered event names - /// in registration order. + /// file means `undefined` from `getSessionFile()`. Print the registered + /// event names in registration order. const OMP_HOST_DRIVER: &str = r#"import register from "./omp-capture.mjs"; const handlers = {}; @@ -360,12 +358,12 @@ register({ on: (event, handler) => (handlers[event] = handler) }); const [event, kind, sessionId, sessionFile] = process.argv.slice(2); const ctx = { cwd: "/work/proj", + agent: { kind }, sessionManager: { getSessionId: () => sessionId, getSessionFile: () => sessionFile || undefined, }, }; -if (kind) ctx.agent = { kind }; handlers[event]({ type: event }, ctx); console.log(Object.keys(handlers).join(" ")); "#; @@ -928,15 +926,9 @@ console.log(Object.keys(handlers).join(" ")); } assert!(!orphan.parent().unwrap().exists()); - // Use an existing file for the sub-agent and absent-`ctx.agent` cases - // to test rejection by agent kind alone. For the top-level cases, - // omit the file on disk. - for (kind, file) in [ - ("sub", session.as_str()), - ("", session.as_str()), - ("main", unwritten), - ("main", ""), - ] { + // Use an existing file for the sub-agent case to test rejection by + // agent kind alone. For the top-level cases, omit the file on disk. + for (kind, file) in [("sub", session.as_str()), ("main", unwritten), ("main", "")] { for event in ["session_start", "agent_end"] { fire(event, kind, ID, file, Some(&cap)); assert!(!cap.exists(), "{event} {kind:?} {file:?} must not report"); diff --git a/src/harness/omp.rs b/src/harness/omp.rs index 9e46d9a..d228e10 100644 --- a/src/harness/omp.rs +++ b/src/harness/omp.rs @@ -7,8 +7,6 @@ //! the same handlers are registered for them, but their IDs are not resumable. //! Wait until the session file is created after the first assistant message. //! For a fresh launch, no ID is captured until the end of the first turn. -//! Capture is unavailable before omp 18.3.2: without the agent kind in the -//! extension context, top-level sessions cannot be distinguished from sub-agents. //! //! omp's IDs are `UUIDv7`. [`is_uuid`](super::is_uuid) validates the 8-4-4-4-12 //! lowercase-hex shape and not the version field, so they pass unchanged. diff --git a/src/harness/summary.rs b/src/harness/summary.rs index 64c5830..63c1e0e 100644 --- a/src/harness/summary.rs +++ b/src/harness/summary.rs @@ -44,8 +44,6 @@ pub const AWAITING_APPROVAL: &str = "awaiting approval"; /// Classify each emitted anchor rule ID as working or idle. Return `None` for IDs no /// adapter emits. List every emitted ID so the corpus test covers each classification. -/// Treat `grok:still-running` as idle because background subagents may remain active -/// after the turn ends. pub fn rule_reports_working(rule: &str) -> Option { match rule { "claude:spinner" | "claude:action-row" | "codex:working" | "grok:spinner" => Some(true), @@ -54,8 +52,7 @@ pub fn rule_reports_working(rule: &str) -> Option { | "codex:approval-menu" | "codex:ran" | "codex:worked" - | "grok:worked" - | "grok:still-running" => Some(false), + | "grok:worked" => Some(false), _ => None, } } @@ -728,13 +725,13 @@ fn codex_working(header: &str, after_paren: &str) -> String { // ------------------------------------------------------------------ grok -- /// grok (alt screen). Use the bordered input box as the structural anchor. Read -/// the status from the first painted row above the box's top border: a braille -/// spinner while working, `Worked for {n}s` after a turn, or `◎ … still running` -/// / `◎ waiting` while background work is live. Limit this tier to working and -/// completion rows. Handle the three blocked states (permission prompt, plan -/// approval, question) through title normalization: expect a `┃`-fenced dialog -/// in place of the box for permission prompts and questions, and `◆` in the -/// probe position for plan approval. See [`SummaryAdapter::normalize_title`]. +/// the status from the first painted row above the box's top border, skipping +/// the Tasks panel: a braille spinner while working or `Worked for {n}s` after +/// a turn. Limit this tier to working and completion rows. Handle the three +/// blocked states (permission prompt, plan approval, question) through title +/// normalization: expect a `┃`-fenced dialog in place of the box for +/// permission prompts and questions, and `◆` in the probe position for plan +/// approval. See [`SummaryAdapter::normalize_title`]. pub struct GrokSummary; impl SummaryAdapter for GrokSummary { @@ -743,18 +740,35 @@ impl SummaryAdapter for GrokSummary { // One probe row: the first painted row above the box. The splash // panel's hints and the session header land here in non-working // states and match neither shape. - let probe = rows[..top].iter().rev().find(|r| !r.is_empty())?; + let first = rows[..top].iter().rposition(|r| !r.is_empty())?; + // A command sent to the background with Ctrl+B paints a Tasks panel + // between the status row and the box for as long as it runs: + // + // ⠹ Waiting for response… 0.1s 9.5s ⇣19.2k [stop] + // + // ▾ Tasks 1 + // ⸬ Run Sleep for 40 seconds 0s + // + // ╭───…───╮ + // + // The panel is the `▾ Tasks {n}` header and the painted rows under + // it; the task glyph animates (`⸬`, `⁙`, `⋅`, `:`), so the rows + // are not matched by shape. When the first painted row is in that + // run, the probe is the first painted row above the header: still + // one row, never a scan. + let end = (0..=first) + .rev() + .take_while(|&i| !rows[i].is_empty()) + .find(|&i| grok_tasks_header(&rows[i])) + .unwrap_or(first + 1); + let probe = rows[..end].iter().rev().find(|r| !r.is_empty())?; let t = probe.trim_start(); // Keep the label through its first ellipsis. Wrapped tail rows have no // spinner prefix, so they fail the frame check and fall through. if let Some(text) = spinner_text(t, braille_frame) { return Some((text, "grok:spinner")); } - // Still-running is the same probe, never a scan: a closer spinner - // or Worked-for row already returned above. - grok_worked(t) - .then(|| (t.to_string(), "grok:worked")) - .or_else(|| grok_still_running(t).map(|text| (text, "grok:still-running"))) + grok_worked(t).then(|| (t.to_string(), "grok:worked")) } fn model_label(&self, rows: &[String]) -> Option { @@ -829,29 +843,14 @@ fn grok_worked(t: &str) -> bool { }) } -/// Background-task chrome grok paints above the box while the main turn -/// looks idle. The `◎` head and the ` · send a message to interrupt` hint -/// drop; `waiting` and `{count} still running` stay. Scrollback such as -/// `Subagent running:` has no `◎` and never matches. -fn grok_still_running(t: &str) -> Option { - let rest = t.strip_prefix("◎ ")?; - let rest = rest - .strip_suffix(" · send a message to interrupt") - .unwrap_or(rest); - if rest == "waiting" { - return Some(rest.to_string()); - } - let body = rest.strip_suffix(" still running")?; - body.split(" · ") - .all(grok_still_running_count) - .then(|| rest.to_string()) -} - -/// One count segment: ASCII digits, a space, then one to three words. -fn grok_still_running_count(seg: &str) -> bool { - after_digits(seg) - .and_then(|r| r.strip_prefix(' ')) - .is_some_and(|words| (1..=3).contains(&words.split_whitespace().count())) +/// The Tasks panel header grok 1.0.50 paints above the box while a +/// backgrounded command runs: `▾ Tasks {n}` with an ASCII-digit count and +/// nothing after it. +fn grok_tasks_header(row: &str) -> bool { + row.trim() + .strip_prefix("▾ Tasks ") + .and_then(after_digits) + .is_some_and(str::is_empty) } /// `╰──── Grok 4.5 (xhigh) · always-approve ─╯` → `Grok 4.5 (xhigh)`: the text grok diff --git a/src/harness/summary_tests.rs b/src/harness/summary_tests.rs index 51188e3..384c8d0 100644 --- a/src/harness/summary_tests.rs +++ b/src/harness/summary_tests.rs @@ -1223,7 +1223,8 @@ fn codex_requires_the_composer_pin() { } /// Spinner label cut at its `…`; the completion row kept verbatim, -/// longer durations included; free text above the box refuses. +/// longer durations included; free text above the box refuses. The Tasks +/// panel is skipped to the one row above its header. #[test] fn grok_status_shapes() { let probe = |status: &str| GrokSummary.live_preview(&grok_screen(&[status, ""])); @@ -1249,70 +1250,66 @@ fn grok_status_shapes() { None ); + // The Tasks panel of a backgrounded command: the probe is the one row + // above its header, whatever that row is. + let panel = |above: &str| { + GrokSummary.live_preview(&grok_screen(&[ + above, + "", + " ▾ Tasks 2", + " ⸬ Run Sleep for 40 seconds 0s", + " ⁙ Run cargo build 3s", + "", + ])) + }; assert_eq!( - GrokSummary.model_label(&grok_screen(&[" ⠋ Thinking… 0.2s", ""])), - Some("Grok 4.5 (xhigh)".to_string()) - ); - // A plain border carries no label. - let plain = rs(&[" ⠋ Thinking… 0.2s", "", "╭────╮", "│ ❯ │", "╰────╯"]); - assert_eq!(GrokSummary.model_label(&plain), None); -} - -/// Still-running chrome: `◎` plus a count phrase or `waiting`. Drop the interrupt hint; -/// body-shaped lookalikes and pre-0.2.109 wording do not match. Prefer a closer -/// Worked-for row: no upward scan. -#[test] -fn grok_still_running_shapes() { - let probe = |status: &str| GrokSummary.live_preview(&grok_screen(&[status, ""])); - assert_eq!( - probe(" ◎ 1 subagent still running"), - Some(("1 subagent still running".to_string(), "grok:still-running")) - ); - assert_eq!( - probe(" ◎ 1 command · 2 monitors · 1 loop · 1 subagent still running"), - Some(( - "1 command · 2 monitors · 1 loop · 1 subagent still running".to_string(), - "grok:still-running" - )) - ); - assert_eq!( - probe(" ◎ 1 command still running · send a message to interrupt"), - Some(("1 command still running".to_string(), "grok:still-running")) - ); - assert_eq!( - probe(" ◎ waiting · send a message to interrupt"), - Some(("waiting".to_string(), "grok:still-running")) - ); - assert_eq!( - probe(" ◎ waiting"), - Some(("waiting".to_string(), "grok:still-running")) + panel(" ⠹ Waiting for response… 0.1s 9.5s ⇣19.2k [stop]"), + Some(("Waiting for response…".to_string(), "grok:spinner")) ); - - for row in [ - " 1 subagent still running", - " watching · 1 subagent", - " Subagent running: \"do the thing\"", - " ◎ still running", - " ◎ 1 to finish", - " ◎ 1 command still running · leftover", - " ◎ 1 still running", - " ◎ command still running", - " ◎ 1 a b c d still running", + assert_eq!(panel(" Done."), None); + // A header-shaped row with a blank row below it is not a panel; a header + // with trailing text is not a header. + for (rows, want) in [ + ( + vec![ + " ⠋ Thinking… 0.2s", + "", + " ▾ Tasks 1", + "", + " Done.", + "", + ], + None, + ), + ( + vec![ + " ⠋ Thinking… 0.2s", + "", + " ▾ Tasks 1 pending", + " Done.", + "", + ], + None, + ), + ( + vec![" ⠋ Thinking… 0.2s", "", " ▾ Tasks 1", " Done.", ""], + Some(("Thinking…".to_string(), "grok:spinner")), + ), ] { - assert_eq!(probe(row), None, "{row:?}"); + assert_eq!( + GrokSummary.live_preview(&grok_screen(&rows)), + want, + "{rows:?}" + ); } - // The probe is a single row: prefer Worked-for closer to the box. - let rows = grok_screen(&[ - " ◎ 1 subagent still running", - "", - " Worked for 8.7s", - "", - ]); assert_eq!( - GrokSummary.live_preview(&rows), - Some(("Worked for 8.7s".to_string(), "grok:worked")) + GrokSummary.model_label(&grok_screen(&[" ⠋ Thinking… 0.2s", ""])), + Some("Grok 4.5 (xhigh)".to_string()) ); + // A plain border carries no label. + let plain = rs(&[" ⠋ Thinking… 0.2s", "", "╭────╮", "│ ❯ │", "╰────╯"]); + assert_eq!(GrokSummary.model_label(&plain), None); } /// Check each observed ` - `-joined title form from grok 1.0.50. Expect the session @@ -1679,11 +1676,20 @@ fn corpus_positive_states_resolve_exactly() { "grok:worked", ), Case( - "preview_grok_still_running", - include_bytes!("../../tests/corpus/preview_grok_still_running.bin"), + "preview_grok_background_tasks", + include_bytes!("../../tests/corpus/preview_grok_background_tasks.bin"), + &GrokSummary, + // The spinner row is read over the Tasks panel of the + // backgrounded command. + "Grok 4.7 (high) · Waiting for response…", + "grok:spinner", + ), + Case( + "preview_grok_waiting_subagent", + include_bytes!("../../tests/corpus/preview_grok_waiting_subagent.bin"), &GrokSummary, - "Grok 4.5 (xhigh) · 1 subagent still running", - "grok:still-running", + "Grok 4.7 (high) · Waiting for subagent…", + "grok:spinner", ), ]; // Exercise both title paths against screens without structural anchors: @@ -1786,8 +1792,6 @@ fn rule_reports_working_classifies_every_emitted_id() { ("codex:ran", false), ("codex:worked", false), ("grok:worked", false), - // Background subagents are live, but the turn itself has ended. - ("grok:still-running", false), ]; for (rule, working) in table { assert_eq!(rule_reports_working(rule), Some(working), "{rule}"); @@ -1934,8 +1938,8 @@ fn corpus_body_shaped_text_never_extracts() { ); assert_eq!(got, anchor("gpt-5.6-sol high · Working", "codex:working")); - // Grok scrollback `Subagent running:` with no `◎` chrome: the probe - // is that body row and refuses, same marker fall-through as idle. + // Grok `Subagent running:` as body text above the idle box: the probe + // is that row and refuses, same marker fall-through as idle. let got = corpus( include_bytes!("../../tests/corpus/preview_grok_subagent_scrollback.bin"), &GrokSummary, diff --git a/tests/corpus/README.md b/tests/corpus/README.md index 00d9ed9..c1ab9b7 100644 --- a/tests/corpus/README.md +++ b/tests/corpus/README.md @@ -106,8 +106,9 @@ prevent a byte-for-byte match with the captured header. | `preview_claude_workflow_wait.bin` | 2.1.215, not re-observed on 2.1.296: waiting on a dynamic workflow, with 19 blank rows before the input box and a workflow roster below it | blank rows skipped during `claude:waiting` matching; the roster is excluded | | `preview_grok_working.bin` | grok braille spinner with elapsed/throughput ticker | `grok:spinner` cut at the label's `…`; border label read | | `preview_grok_worked.bin` | grok `Worked for 8.7s` completion row above the box | `grok:worked` kept verbatim | -| `preview_grok_still_running.bin` | grok `◎ 1 subagent still running` above the idle box | `grok:still-running`; border label read | -| `preview_grok_subagent_scrollback.bin` | grok idle with `Subagent running:` in the body, no `◎` row | fall-through; body-shaped text is not status | +| `preview_grok_background_tasks.bin` | grok 1.0.50, captured 2026-10-10: `⠹ Waiting for response…` spinner over the `▾ Tasks 1` panel of a command sent to the background with Ctrl+B; `⠋ - Waiting for response… - … - grok` title announce | `grok:spinner` read over the Tasks panel; border label read | +| `preview_grok_waiting_subagent.bin` | grok 1.0.50, captured 2026-10-10: `⠴ Waiting for subagent…` spinner while a subagent runs; `⠙ - Waiting for subagent… - grok` title announce | `grok:spinner`; a subagent turn is an ordinary spinner row | +| `preview_grok_subagent_scrollback.bin` | grok idle with `Subagent running:` as body text above the box | fall-through; body-shaped text is not status | | `preview_grok_idle.bin` | grok idle session | fall-through to the marker | | `preview_grok_splash.bin` | grok launch splash with resume hint above the box | fall-through; distinct views excluded from anchors | | `preview_grok_idle_titled.bin` | `preview_grok_idle.bin` plus a `Run ls -la list directory file names - grok` title announce | Strip `- grok` and render the session name through the title tier | diff --git a/tests/corpus/preview_grok_background_tasks.bin b/tests/corpus/preview_grok_background_tasks.bin new file mode 100644 index 0000000..b734804 --- /dev/null +++ b/tests/corpus/preview_grok_background_tasks.bin @@ -0,0 +1,39 @@ +[?1049h]0;⠋ - Waiting for response… - Run sleep 40 command then say done - grok + ~/Projects/fixture-sources/grok 19K / 256K │ [Dashboard] + + + ❯ Run the shell command sleep 40 and then say done. 4:30 PM + + + ◆ Thought for 2.1s + + I'll run sleep 40 and report back when it finishes. 4:30 PM + + ◆ Task started: Sleep for 40 seconds + + + + + + + + + + + + + + + + + + ⠹ Waiting for response… 0.1s 9.5s ⇣19.2k [stop] + + ▾ Tasks 1 + ⸬ Run Sleep for 40 seconds 0s + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ ❯ │ + ╰────────────────────────────────────────────────────────────────────────────────── Grok 4.7 (high) · auto-review ─╯ + + Shift+Tab:mode │ Ctrl+c:cancel │ Ctrl+.:shortcuts diff --git a/tests/corpus/preview_grok_still_running.bin b/tests/corpus/preview_grok_still_running.bin deleted file mode 100644 index b9bd28b..0000000 --- a/tests/corpus/preview_grok_still_running.bin +++ /dev/null @@ -1,39 +0,0 @@ -[?1049h - ~/Projects/fixture-sources 3.0K / 500K - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ◎ 1 subagent still running - ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ ❯ │ - ╰────────────────────────────────────────────────────────────────────────────── Grok 4.5 (xhigh) · always-approve ─╯ - - Shift+Tab:mode │ Ctrl+x:shortcuts diff --git a/tests/corpus/preview_grok_waiting_subagent.bin b/tests/corpus/preview_grok_waiting_subagent.bin new file mode 100644 index 0000000..9c1b2ea --- /dev/null +++ b/tests/corpus/preview_grok_waiting_subagent.bin @@ -0,0 +1,39 @@ +[?1049h]0;⠙ - Waiting for subagent… - grok + ~/Projects/fixture-sources/grok 19K / 256K │ [Dashboard] + + + ❯ Launch one subagent whose only job is to reply with the word ok, then tell me what it said. 4:27 PM + + + ◆ Thought for 0.0s + + I'll launch one subagent whose only task is to reply with "ok", then report exactly what it returns. 4:27 PM + + + + + + + + + + + + + + + + + + + + + + + ⠴ Waiting for subagent… 0.0s 2.9s ⇣19.1k [stop] + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ ❯ │ + ╰────────────────────────────────────────────────────────────────────────────────── Grok 4.7 (high) · auto-review ─╯ + + Shift+Tab:mode │ Ctrl+c:cancel │ Ctrl+.:shortcuts From a06a2b642e7849caae9219bddaa09bb45124dfed Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 10 Oct 2026 23:02:59 -0700 Subject: [PATCH 5/5] Polish documentation and comments across multiple files for clarity and consistency --- docs/sessions.md | 2 +- src/harness/assets.rs | 2 +- src/harness/codex.rs | 3 ++- src/harness/summary.rs | 33 ++++++++++++------------ src/harness/summary_tests.rs | 50 ++++++++++++++++++------------------ src/session.rs | 12 ++++----- tests/corpus/README.md | 25 +++++++++--------- 7 files changed, 65 insertions(+), 62 deletions(-) diff --git a/docs/sessions.md b/docs/sessions.md index 88c9d14..2459064 100644 --- a/docs/sessions.md +++ b/docs/sessions.md @@ -46,7 +46,7 @@ A session file is a JSON object with three fields: `version` for the format vers - Values are ordered lists, loaded in order. Each task is started in its own PTY under the specified directory. - Directories are serialized alphabetically, with entry order preserved within each directory. -The `version` field must be an integer from 1 through the newest format supported by the running `fleetcom`. A missing field means version 1. A version 1 file loads with every entry literal, including `claude` and `claude --resume ''`; saving writes those back as literals, so to track such a task's conversation, launch it from the Agent page and save again. Invalid or unsupported versions are refused on load, with the file's value and supported version in the error. A version 2 file read by an older `fleetcom` is refused by that same gate. +The `version` field must be an integer from 1 through the newest format supported by the running `fleetcom`. A missing field means version 1. A version 1 file loads with every entry literal, including `claude` and `claude --resume ''`, and they are written back as literals on save; to track such a task's conversation, launch it from the Agent page and save again. Invalid or unsupported versions are refused on load, with the file's value and supported version in the error. A version 2 file read by an older `fleetcom` is refused by that same gate. The schema is identified by shape, independently of the version. With an object-valued `dirs`, the wrapped form above is used. A flat map is also accepted, with directories as top-level keys and entry arrays as values. In that form, an array-valued key named `dirs` remains a directory entry, but a top-level `version` member is always the format version, never a directory. Flat-map files are listed by filename stem because no name is stored. On save, the wrapped form is written, without a stored-name collision check. diff --git a/src/harness/assets.rs b/src/harness/assets.rs index 2fb1f1a..d46ee35 100644 --- a/src/harness/assets.rs +++ b/src/harness/assets.rs @@ -105,7 +105,7 @@ function write(ctx, reason) { const path = process.env.FLEETCOM_CAPTURE_FILE; if (!path) return; // These handlers are also registered for sub-agent sessions, which cannot - // be resumed by ID: only the top-level session reports. + // be resumed by ID: report only the top-level session. if (ctx.agent?.kind !== "main") return; // The session file is created after the first assistant message. Until // then, the ID cannot be found through `omp --resume`. diff --git a/src/harness/codex.rs b/src/harness/codex.rs index b9f4ef1..839b2c1 100644 --- a/src/harness/codex.rs +++ b/src/harness/codex.rs @@ -177,7 +177,8 @@ fn replace_slot(capture: &Path, root: &str) -> io::Result<()> { /// `source` object. /// /// Return `None` unless exactly one rollout is named for `thread` and its -/// header names `thread` itself or a sub-agent's root. No rollout is saved +/// header's `session_id` is `thread` itself or, under a `subagent` source, the +/// root. No rollout is saved /// for the title thread. Examine only directory listings and the first line. fn root_thread(home: &Path, thread: &str) -> Option { let mut rollouts = Vec::new(); diff --git a/src/harness/summary.rs b/src/harness/summary.rs index 63c1e0e..81cb57a 100644 --- a/src/harness/summary.rs +++ b/src/harness/summary.rs @@ -307,10 +307,10 @@ const CLAUDE_EFFORT: &[&str] = &["low", "medium", "high", "xhigh", "max"]; /// ▝▝ ▝▝ /…/scratchpad/claude-work · /rc /// ``` /// -/// The model row follows the `Claude Code v` row, which sits within the first +/// The model row is the row after `Claude Code v`, found within the first /// four rows; its first ` · ` segment is the model text. The header is the -/// stable source; user-configurable statusline rows are not parsed. When the -/// header scrolls away, the label is unavailable. +/// stable source; user-configurable statusline rows are not parsed. With the +/// header scrolled out of view, no label is available. fn claude_welcome_label(rows: &[String]) -> Option { let version = rows .iter() @@ -388,13 +388,14 @@ impl SummaryAdapter for CodexSummary { /// Codex 0.162.1 titles the terminal `[head ]summary | cwd`, where `cwd` is /// the working directory's basename and `head` is a braille frame or the - /// blocked blink `[ . ] `/`[ ! ] `. One turn shows four titles: `codex-work` - /// at startup, `⠴ ⠴ | codex-work` for the first seconds of the turn (the - /// summary is a frame placeholder until the turn is named), `⠙ Count to - /// twenty slowly | codex-work` while working, `Count to twenty slowly | - /// codex-work` after. Return the summary, split at the last ` | `, with - /// `working` set for a frame head; fold the blink to its `[ ! ] ` phase. - /// Return `None` for the bare cwd (the alternate screen keeps it verbatim) + /// blocked blink `[ . ] `/`[ ! ] `. Across one turn the title reads + /// `codex-work` at startup, `⠴ ⠴ | codex-work` for the first seconds of + /// the turn (the summary is a frame placeholder until the turn is named), + /// `⠙ Count to twenty slowly | codex-work` while working, and `Count to + /// twenty slowly | codex-work` after. Return the summary, split at the last + /// ` | `, with `working` set for a frame head; fold the blink to its + /// `[ ! ] ` phase. Return `None` for the bare cwd (rendered verbatim on the + /// alternate screen) /// and for a placeholder or empty summary: the working signal is lost for /// the placeholder seconds rather than a frame rendering as text. A headed /// title without ` | ` is all summary. @@ -603,10 +604,10 @@ fn codex_composer(rows: &[String]) -> Option { /// [`CODEX_QUEUED_HEADS`] are walked past, then inspect the first other column-0 row. /// Extract only [`codex_worked`], [`codex_status_head`], or `• Ran ` shapes; stop at /// any other column-0 row (a reply bullet, a `⚠` notice, a turn separator): `• Ran` -/// rows from prior turns are retained in scrollback, and skipping an unknown row to -/// reach one would resurface stale work as live status. The completion row is +/// rows from prior turns are retained in scrollback; skip past an unknown row to +/// reach one and stale work is reported as live status. The completion row is /// indented, so it is tested before the indentation skip. `• Ran ` is tested before -/// the status head because the head matches on structure, not on a literal verb. +/// the status head, which is matched on structure, not on a literal verb. fn codex_status(rows: &[String], composer: usize) -> Option<(String, &'static str)> { // Indented rows crossed since the last column-0 row. Exclude rows below a queued // head from the count to avoid exhausting the window on a deep queue. @@ -741,8 +742,8 @@ impl SummaryAdapter for GrokSummary { // panel's hints and the session header land here in non-working // states and match neither shape. let first = rows[..top].iter().rposition(|r| !r.is_empty())?; - // A command sent to the background with Ctrl+B paints a Tasks panel - // between the status row and the box for as long as it runs: + // While a command sent to the background with Ctrl+B runs, grok paints + // a Tasks panel between the status row and the box: // // ⠹ Waiting for response… 0.1s 9.5s ⇣19.2k [stop] // @@ -752,7 +753,7 @@ impl SummaryAdapter for GrokSummary { // ╭───…───╮ // // The panel is the `▾ Tasks {n}` header and the painted rows under - // it; the task glyph animates (`⸬`, `⁙`, `⋅`, `:`), so the rows + // it; the task glyph is animated (`⸬`, `⁙`, `⋅`, `:`), so the rows // are not matched by shape. When the first painted row is in that // run, the probe is the first painted row above the header: still // one row, never a scan. diff --git a/src/harness/summary_tests.rs b/src/harness/summary_tests.rs index 384c8d0..1cced9d 100644 --- a/src/harness/summary_tests.rs +++ b/src/harness/summary_tests.rs @@ -309,7 +309,7 @@ fn claude_title_frames_canonicalize_to_constant_text() { "{frame:?}" ); } - // 2.1.296 names the session once the first turn starts; the frame stays `✳`. + // 2.1.296 names the session once the first turn starts, with the frame still `✳`. assert_eq!( ClaudeSummary.normalize_title("✳ Directory listing with ls -la"), Some(("Directory listing with ls -la".to_string(), false)) @@ -325,8 +325,8 @@ fn claude_title_frames_canonicalize_to_constant_text() { "quadrant frames must render one string and report working" ); - // Braille frames belong to the 2.1.209 title animation, which 2.1.296 no - // longer emits: refuse rather than guess a state. + // Braille frames are the 2.1.209 title animation, which 2.1.296 no longer + // emits: refuse rather than guess a state. for frame in BRAILLE_FRAMES.iter().chain(['⠐'].iter()) { assert_eq!( ClaudeSummary @@ -547,7 +547,7 @@ fn claude_label_reads_the_logo_header() { "▝▜██████▀ Opus 5 (1M context) with high effort · Claude Max", Some("Opus 5 (1M context) (high)"), ), - // A narrow terminal cuts the row with the CLI's own ellipsis. + // At a narrow width the CLI truncates the row with its own ellipsis. ( "▝▜██████▀ Opus 5 (1M context) with high…", Some("Opus 5 (1M context) (high)"), @@ -565,7 +565,7 @@ fn claude_label_reads_the_logo_header() { ); } - // The model row must follow the version row directly. + // The model row is the one directly after the version row. let gapped = rs(&[ " ▐▛███▛█ Claude Code v2.1.296", "", @@ -573,7 +573,7 @@ fn claude_label_reads_the_logo_header() { ]); assert_eq!(ClaudeSummary.model_label(&gapped), None); - // A header below the fourth row has scrolled into the body. + // A header below the fourth row is body text, not the header. let scrolled = rs(&[ "", "", @@ -769,17 +769,17 @@ fn codex_label_rejects_warning_prose_and_incomplete_footers() { assert_eq!(CodexSummary.model_label(&rows), None); } -/// The 0.162.1 title grammar is `[frame ]summary | cwd`: the summary is kept, -/// the cwd dropped, a frame marks working, and every frame renders one string. -/// The bare cwd and the frame placeholder yield nothing; the blocked blink -/// folds to one phase. +/// The 0.162.1 title grammar is `[frame ]summary | cwd`: keep the summary, +/// drop the cwd, set `working` for a frame, and render every frame to the same +/// string. Return `None` for the bare cwd and the frame placeholder; fold the +/// blocked blink to one phase. #[test] fn codex_title_grammar_yields_the_summary() { let t = |title: &str| CodexSummary.normalize_title(title); let named = Some(("Count to twenty slowly".to_string(), true)); let idle = Some(("Count to twenty slowly".to_string(), false)); - // The four titles one turn shows, in order. + // The four title forms of one turn, in order. assert_eq!(t("codex-work"), None, "the cwd basename is not a summary"); assert_eq!( t("⠴ ⠴ | codex-work"), @@ -807,13 +807,13 @@ fn codex_title_grammar_yields_the_summary() { ); } - // The cwd is split off at the last separator; the summary keeps its own. + // The cwd is split off at the last ` | `; any earlier ` | ` is part of the summary. assert_eq!( t("⠙ Fix a | b | codex-work"), Some(("Fix a | b".to_string(), true)) ); - // Both blink phases render the `[ ! ]` phase, with and without a cwd. + // Both blink phases are rendered as the `[ ! ]` phase, with and without a cwd. for title in [ "[ . ] Action Required | fleetcom", "[ ! ] Action Required | fleetcom", @@ -826,7 +826,7 @@ fn codex_title_grammar_yields_the_summary() { ); } - // Foreign, frame-only, empty-summary, and empty titles do not match. + // No match for foreign, frame-only, empty-summary, and empty titles. assert_eq!(t("zellij: main"), None); assert_eq!(t("⠹"), None, "frame alone"); assert_eq!(t("⠹ "), None, "frame with an empty rest"); @@ -1223,8 +1223,8 @@ fn codex_requires_the_composer_pin() { } /// Spinner label cut at its `…`; the completion row kept verbatim, -/// longer durations included; free text above the box refuses. The Tasks -/// panel is skipped to the one row above its header. +/// longer durations included; free text above the box is refused. The Tasks +/// panel is skipped: the probe is the one row above its header. #[test] fn grok_status_shapes() { let probe = |status: &str| GrokSummary.live_preview(&grok_screen(&[status, ""])); @@ -1307,7 +1307,7 @@ fn grok_status_shapes() { GrokSummary.model_label(&grok_screen(&[" ⠋ Thinking… 0.2s", ""])), Some("Grok 4.5 (xhigh)".to_string()) ); - // A plain border carries no label. + // No label from a plain border. let plain = rs(&[" ⠋ Thinking… 0.2s", "", "╭────╮", "│ ❯ │", "╰────╯"]); assert_eq!(GrokSummary.model_label(&plain), None); } @@ -1580,8 +1580,8 @@ fn corpus_positive_states_resolve_exactly() { "preview_claude_action", include_bytes!("../../tests/corpus/preview_claude_action.bin"), &ClaudeSummary, - // The spinner row's `thought for 2s` segment rides along; its - // elapsed and token tickers do not. + // `thought for 2s` is kept from the spinner row; the elapsed and + // token tickers are dropped. "Fable 5.1 (high) · Running 1 shell command… · thought for 2s", "claude:action-row", ), @@ -1811,8 +1811,8 @@ fn rule_reports_working_classifies_every_emitted_id() { /// alternate-screen marker. The claude idle screen is the launch state (`Try /// "…"` placeholder, `● high · /effort` hint row, header still connecting); /// the done screen has `✻ Sautéed for 17s · done 4:14 PM` as the first -/// column-0 row above a box holding a typed draft: no `…`, so the scan -/// rejects it as foreign. The codex idle screen is the 0.162.1 launch state: +/// column-0 row above a box holding a typed draft: no `…`, so it is rejected +/// as a foreign row. The codex idle screen is the 0.162.1 launch state: /// the command list above the placeholder composer, nothing between them. #[test] fn corpus_idle_states_fall_through() { @@ -1911,8 +1911,8 @@ fn corpus_body_shaped_text_never_extracts() { ); // The same on 0.162.1's alternate screen: `• Ran curl …` and its `└` - // output sit behind the `• HTTP/2 200` reply bullet, so the scan stops - // there and the marker reports. + // output are behind the `• HTTP/2 200` reply bullet, so the scan stops + // at the bullet and the marker is reported. let got = corpus( include_bytes!("../../tests/corpus/preview_codex_ran_done.bin"), &CodexSummary, @@ -1938,8 +1938,8 @@ fn corpus_body_shaped_text_never_extracts() { ); assert_eq!(got, anchor("gpt-5.6-sol high · Working", "codex:working")); - // Grok `Subagent running:` as body text above the idle box: the probe - // is that row and refuses, same marker fall-through as idle. + // Grok `Subagent running:` as body text above the idle box: that row is + // the probe and is refused, the same marker fall-through as idle. let got = corpus( include_bytes!("../../tests/corpus/preview_grok_subagent_scrollback.bin"), &GrokSummary, diff --git a/src/session.rs b/src/session.rs index 0208600..9be313a 100644 --- a/src/session.rs +++ b/src/session.rs @@ -109,9 +109,9 @@ pub fn sessions_dir(root: Option) -> Option { /// Session format version written by `to_json` and accepted by `from_json`. Managed entries /// were introduced in version 2. A missing version means 1; `from_json` loads a v1 file -/// verbatim, every entry literal. Through 0.15.0 the loader rewrote a v1 file's bare agent +/// verbatim, every entry literal. Through 0.15.0 `from_json` rewrote a v1 file's bare agent /// words and canonical resume commands into managed entries, because v1 had no managed form; -/// the release after 0.15.0 retired that rewrite, so an unconverted v1 file loads without +/// that rewrite was retired in the release after 0.15.0, so an unconverted v1 file loads without /// managed resume tracking, and `to_json` writes its entries back as literals. Reject newer /// versions. const FORMAT_VERSION: u64 = 2; @@ -175,8 +175,8 @@ fn from_json(text: &str) -> io::Result<(Option, SessionConfig)> { if !parsed.is_object() { return Err(invalid("session root: expected an object".into())); } - // Validate version metadata before detecting the schema shape. The value gates - // nothing else: every accepted version shares one entry grammar. + // Validate version metadata before detecting the schema shape. Only the range + // is checked: every accepted version has the same entry grammar. if parsed.has_key("version") { match parsed["version"].as_u64() { Some(n) if (1..=FORMAT_VERSION).contains(&n) => {} @@ -749,8 +749,8 @@ mod tests { } /// Preserve literal agent words and resume commands in every accepted version. A v1 - /// file now loads verbatim: the rewrite to managed entries is gone, so the spellings - /// it once converted stay literal beside a v2 managed entry. + /// file now loads verbatim: with the rewrite to managed entries gone, the spellings + /// it once converted are literal beside a v2 managed entry. #[test] fn literal_agent_words_stay_literal_in_every_version() { for version in ["", r#""version": 1, "#, r#""version": 2, "#] { diff --git a/tests/corpus/README.md b/tests/corpus/README.md index c1ab9b7..cb192ab 100644 --- a/tests/corpus/README.md +++ b/tests/corpus/README.md @@ -38,7 +38,7 @@ normalization, and fallback behavior. Each fixture is a constructed repaint stream: optional alternate-screen entry, clear, home, then sanitized screen rows joined with CRLF. Claude and Grok use the alternate screen. Codex 0.162.1 runs on the alternate screen from startup, so the five codex fixtures captured on it -(`idle`, `working`, `queued`, `done`, `ran_done`) open with `1049h`; the older +(`idle`, `working`, `queued`, `done`, `ran_done`) are prefixed with `1049h`; the older codex fixtures and omp are inline. Identifying and user-configured text is replaced with alignment-preserving synthetic values. Geometry is 40×120 unless noted. @@ -47,18 +47,19 @@ Geometry is 40×80 for `preview_codex_reasoning.bin`, bottom-anchored on a 40-row screen to reproduce the inline layout. Indentation in the Codex hint-row and approval fixtures is approximate; match -trimmed heads and column-0 structure in their tests. The codex 0.162.1 fixtures -open on the `>_ OpenAI Codex (v0.162.1)` header with a same-width synthetic -working directory on the row below it; the `GPT-6-Luna max · … in · … out` -status line, token counters, clock times, and prompts are verbatim. The Claude fixtures with a -model prefix open on claude 2.1.296's three-row logo header (`Claude Code +trimmed heads and column-0 structure in their tests. In the codex 0.162.1 +fixtures, the first painted row is the `>_ OpenAI Codex (v0.162.1)` header, +with a same-width synthetic working directory on the row below it; the +`GPT-6-Luna max · … in · … out` status line, token counters, clock times, and +prompts are verbatim. In the Claude fixtures with a model prefix, the first +painted rows are claude 2.1.296's three-row logo header (`Claude Code v2.1.296`, `Fable 5.1 with high effort · Claude Max`, working directory); the working directory on the header row and on the statusline row below the input -box is a same-width synthetic path. Fixtures captured on 2.1.296 omit the -capture environment's `⚠ Transcript saving is off` statusline row, leaving it -empty; the three fixtures whose body is from 2.1.215 carry the 2.1.296 header -transplanted over the old welcome box, with the box's remaining rows left empty -so the body keeps its row indexes. In the Claude waiting fixture, the header is +box is a same-width synthetic path. In the fixtures captured on 2.1.296, the +capture environment's `⚠ Transcript saving is off` statusline row is omitted +and left empty; in the three fixtures whose body is from 2.1.215, the 2.1.296 +header is transplanted over the old welcome box, with the box's remaining rows +left empty so the body's row indexes are unchanged. In the Claude waiting fixture, the header is omitted and agent-roster rows are included below the input box. In the Claude task-list fixtures, generic phase names are used in a task-list layout and the header is omitted. In the Claude workflow-wait fixture, generic wording is used, @@ -130,7 +131,7 @@ The versioned directories contain raw PTY streams from the official macOS arm64 0.158.0 release executable, recorded on 2026-09-28. Codex ran with isolated homes, `--no-daemon`, and a loopback Responses server supplying scripted replies. The 0.157.1 streams recorded alongside them were deleted on -2026-10-10: no matcher arm depended on them alone, and 0.158.0 itself is +2026-10-10: nothing in the matchers was pinned by them alone, and 0.158.0 itself is behind the current release, 0.162.1. - [`codex_0158_preview`](codex_0158_preview/README.md) covers working status,