From 7686b99e9bc48a85af11a375ea61722e8559425c Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sun, 16 Aug 2026 15:40:37 -0700 Subject: [PATCH 1/6] feat(emulator): retain the last primary-screen title past its staging --- src/terminal/emulator.rs | 92 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/src/terminal/emulator.rs b/src/terminal/emulator.rs index da1a273..eb027c0 100644 --- a/src/terminal/emulator.rs +++ b/src/terminal/emulator.rs @@ -540,6 +540,18 @@ impl Emulator { .map(|t| t.text.as_str()) } + /// The last sanitized title announced on the primary screen, retained + /// across printable output and alternate-screen excursions. An empty + /// announce or a full reset clears it. [`Emulator::title`] serves the + /// staged/epoch view; this slot serves inline programs whose constant + /// repaints disclaim staging. + // Unused pub in a bin crate is dead code; the preview resolution layer + // consumes this once its phase lands. + #[allow(dead_code)] + pub fn primary_title(&self) -> Option<&str> { + self.alt.primary_title.as_deref() + } + /// The last non-blank row of the live screen, trailing padding trimmed; /// empty when the screen is blank. Ignores the scrollback view offset: /// `contents` follows `display_offset`, which would make a scrolled-back @@ -626,6 +638,12 @@ struct AltScreen { /// alternate-screen entry. Printable output disclaims it; a reset clears /// it. staged_title: Option, + /// Retained last sanitized primary-screen announce. Where `staged_title` + /// bridges an announce into the next alternate-screen entry and dies on + /// printable output, this slot persists through output and alt + /// excursions: inline TUIs repaint constantly, so a disclaimed slot + /// could never label them. An empty announce or a reset clears it. + primary_title: Option, /// Mirror of the backend's raw (unsanitized) current title, kept only /// so the title-stack shadow pushes what the backend pushes. raw_title: Option, @@ -683,10 +701,10 @@ impl ObservedTerm<'_> { self.alt.last_alt = alt; } - /// Assign a sanitized title to the current alternate-screen epoch or - /// stage it for the next entry when on the primary screen. An empty title - /// clears captured and staged titles. Printable output, but not control - /// traffic, disclaims a staged title. + /// Assign a sanitized title to the current alternate-screen epoch, or + /// stage and retain it when on the primary screen. An empty title clears + /// captured, staged, and retained titles. Printable output, but not + /// control traffic, disclaims a staged title. fn observe_title(&mut self, title: Option) { self.alt.raw_title.clone_from(&title); let text = title @@ -695,6 +713,7 @@ impl ObservedTerm<'_> { let Some(text) = text else { self.alt.title = None; self.alt.staged_title = None; + self.alt.primary_title = None; return; }; if self.term.mode().contains(TermMode::ALT_SCREEN) { @@ -703,7 +722,8 @@ impl ObservedTerm<'_> { alt_epoch: self.alt.epoch, }); } else { - self.alt.staged_title = Some(text); + self.alt.staged_title = Some(text.clone()); + self.alt.primary_title = Some(text); } } } @@ -785,6 +805,7 @@ impl Handler for ObservedTerm<'_> { self.alt.raw_title = None; self.alt.title_stack.clear(); self.alt.staged_title = None; + self.alt.primary_title = None; } delegate! { reverse_index(); @@ -1683,6 +1704,67 @@ mod tests { assert_eq!(emu.title(), Some("done")); } + /// Printable output disclaims the staged slot and leaves the retained + /// slot alone: the asymmetry that lets an inline TUI's title outlive + /// its own repaints. + #[test] + fn primary_title_survives_the_printable_output_that_disclaims_staging() { + let mut emu = Emulator::new(4, 20, 0); + emu.process(b"\x1b]0;omp\x07"); + assert_eq!(emu.title(), Some("omp"), "premise: the announce staged"); + assert_eq!(emu.primary_title(), Some("omp")); + emu.process(b"$ ls\r\n"); + assert_eq!(emu.title(), None, "staged: disclaimed by printed output"); + assert_eq!(emu.primary_title(), Some("omp"), "retained: survives it"); + } + + /// omp's announce shape: a bare prompt title, printed output, then a + /// labeled re-announce. The newer announce overwrites the retained slot. + #[test] + fn primary_title_is_overwritten_by_a_newer_announce() { + let mut emu = Emulator::new(4, 20, 0); + emu.process("\x1b]0;\u{3c0} >\x07build output\r\n".as_bytes()); + assert_eq!(emu.primary_title(), Some("\u{3c0} >")); + emu.process("\x1b]0;\u{3c0} > check\x07".as_bytes()); + assert_eq!(emu.primary_title(), Some("\u{3c0} > check")); + } + + /// codex's clear shape: an empty announce empties the retained slot; a + /// RIS resets it with the rest of the title state. + #[test] + fn empty_announce_and_reset_clear_the_primary_title() { + let mut emu = Emulator::new(4, 20, 0); + emu.process(b"\x1b]0;codex\x07"); + assert_eq!(emu.primary_title(), Some("codex")); + emu.process(b"\x1b]0;\x07"); + assert_eq!(emu.primary_title(), None, "an empty announce clears"); + + let mut emu = Emulator::new(4, 20, 0); + emu.process(b"\x1b]0;codex\x07"); + emu.process(b"\x1bc"); + assert_eq!(emu.primary_title(), None, "RIS clears"); + } + + /// An alternate-screen excursion — the entry claiming the staged title, + /// an in-alt announce, the exit — leaves the retained slot at the + /// pre-alt announce: a primary title stays true across it. + #[test] + fn primary_title_is_unaffected_by_an_alt_round_trip() { + let mut emu = Emulator::new(4, 20, 0); + emu.process(b"\x1b]0;shell\x07\x1b[?1049h"); + assert_eq!(emu.alt_epoch(), 1); + assert_eq!(emu.title(), Some("shell"), "premise: entry claimed staging"); + emu.process(b"\x1b]0;altapp\x07"); + assert_eq!(emu.title(), Some("altapp")); + assert_eq!( + emu.primary_title(), + Some("shell"), + "an in-alt announce must not touch the slot" + ); + emu.process(b"\x1b[?1049l"); + assert_eq!(emu.primary_title(), Some("shell")); + } + /// The end-of-life landing runs the same bookkeeping as `process`: a /// title and alt entry buffered inside a never-closed ?2026 frame must /// count when `finish_output` lands it. From 07347edc01348fc9f3b27099f7b27963ec27cedf Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sun, 16 Aug 2026 15:40:37 -0700 Subject: [PATCH 2/6] feat(summary): per-CLI normalizers for the primary-screen title tier --- src/harness/summary.rs | 43 ++++++++++++++++--- src/harness/summary_tests.rs | 83 +++++++++++++++++++++++++++++++----- 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/src/harness/summary.rs b/src/harness/summary.rs index 496e914..784f024 100644 --- a/src/harness/summary.rs +++ b/src/harness/summary.rs @@ -128,17 +128,21 @@ impl SummaryAdapter for ClaudeSummary { claude_welcome_label(rows) } - /// Canonicalize a leading claude spinner, braille, or quadrant-circle - /// frame to `✻` so title animation does not change the rendered text. - /// Other titles pass through unchanged. + /// Strip a leading claude spinner, braille, or quadrant-circle frame: + /// the title text carries the information, the frame is animation, so a + /// framed title normalizes to its bare text and animation never changes + /// the rendered result. Other titles pass through unchanged. fn normalize_title(&self, title: &str) -> Option { let mut chars = title.chars(); let frame = chars.next()?; - // Normalize the entire quadrant-circle block as one animation set. + // The quadrant-circle block is one animation set with the spinner. let framed = CLAUDE_SPINNER.contains(&frame) || braille_frame(frame) || ('\u{25D0}'..='\u{25D3}').contains(&frame); - (framed && chars.next()? == ' ').then(|| format!("✻ {}", chars.as_str())) + // A frame with no text refuses: `None` renders the title verbatim, + // where `Some("")` would render a blank preview line. + (framed && chars.next()? == ' ' && !chars.as_str().is_empty()) + .then(|| chars.as_str().to_string()) } } @@ -716,6 +720,35 @@ impl SummaryAdapter for OmpSummary { fn model_label(&self, _rows: &[String]) -> Option { None } + + /// Decode omp's `π {sep} {label}` title. The separator carries the + /// state: `>` is idle, a braille frame is working (folded to `⠋` so + /// animation never changes the rendered text), `!` is waiting on the + /// user; `π: {label}` is the state feature disabled. The label is the + /// information — an idle or feature-disabled title without one carries + /// nothing and refuses. So does any non-`π` title: an extension override + /// owns the title verbatim, and a wrong preview is worse than none. + fn normalize_title(&self, title: &str) -> Option { + if let Some(label) = title.strip_prefix("π: ") { + return (!label.is_empty()).then(|| label.to_string()); + } + let mut chars = title.strip_prefix("π ")?.chars(); + let sep = chars.next()?; + let label = match chars.next() { + None => "", + Some(' ') => chars.as_str(), + Some(_) => return None, + }; + match sep { + '>' => (!label.is_empty()).then(|| label.to_string()), + // `!` and the frame stay: without a label they are the state. + '!' if label.is_empty() => Some("!".to_string()), + '!' => Some(format!("! {label}")), + f if braille_frame(f) && label.is_empty() => Some("⠋".to_string()), + f if braille_frame(f) => Some(format!("⠋ {label}")), + _ => None, + } + } } /// Inspect the bottom-most `╰…╯` row and return its predecessor only when that diff --git a/src/harness/summary_tests.rs b/src/harness/summary_tests.rs index f647f55..2af0c14 100644 --- a/src/harness/summary_tests.rs +++ b/src/harness/summary_tests.rs @@ -275,13 +275,14 @@ fn claude_waiting_family_matches_the_skeleton_and_never_probes() { ); } -/// Every spinner frame canonicalizes to `✻`; non-frame titles pass through. +/// Every spinner frame strips to the bare title text — the text is the +/// information, the frame was decoration; non-frame titles pass through. #[test] fn claude_title_frames_canonicalize_to_constant_text() { for frame in CLAUDE_SPINNER { assert_eq!( ClaudeSummary.normalize_title(&format!("{frame} Claude Code")), - Some("✻ Claude Code".to_string()), + Some("Claude Code".to_string()), "{frame:?}" ); } @@ -298,23 +299,28 @@ fn claude_title_frames_canonicalize_to_constant_text() { .collect(); assert_eq!( rendered, - std::collections::BTreeSet::from([Some("✻ Run sleep command".to_string())]), + std::collections::BTreeSet::from([Some("Run sleep command".to_string())]), "spinner and quadrant frames must render one string" ); // 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()) + Some("Review fleetcom preview design document".to_string()) ); assert_eq!( ClaudeSummary.normalize_title("⠴ Review fleetcom preview design document"), - Some("✻ Review fleetcom preview design document".to_string()), + Some("Review fleetcom preview design document".to_string()), "mid-block braille frame" ); assert_eq!(ClaudeSummary.normalize_title("zellij: main"), None); assert_eq!(ClaudeSummary.normalize_title("✻"), None, "frame alone"); + assert_eq!( + ClaudeSummary.normalize_title("\u{273b} "), + None, + "a frame with no text refuses rather than rendering a blank" + ); } /// A registry status outranks a screen-derived status while retaining the @@ -366,8 +372,8 @@ fn registry_anchor_outranks_the_claude_spinner() { } /// Cascade-level: with the claude adapter installed and no anchor on -/// the screen, a frame-led title renders canonicalized under the Title -/// tier; without an adapter it renders verbatim. +/// the screen, a frame-led title renders stripped to its text under the +/// Title tier; without an adapter it renders verbatim. #[test] fn title_tier_renders_the_normalized_title() { let mut emu = Emulator::new(24, 80, 100); @@ -378,7 +384,7 @@ fn title_tier_renders_the_normalized_title() { .clone(); assert_eq!( (p.text.as_str(), p.source, p.rule), - ("✻ Claude Code", PreviewSource::Title, None) + ("Claude Code", PreviewSource::Title, None) ); let mut st = PreviewState::new(); @@ -389,7 +395,7 @@ fn title_tier_renders_the_normalized_title() { "no adapter: verbatim" ); - // Quadrant frames use the same canonical title as other spinner frames. + // Quadrant frames strip to the same bare text as other spinner frames. let mut quadrant = Emulator::new(24, 80, 100); quadrant.process( b"\x1b[?1049h\x1b]0;\xe2\x97\x90 Run sleep command for 25 seconds\x07conversation body", @@ -401,7 +407,7 @@ fn title_tier_renders_the_normalized_title() { assert_eq!( (p.text.as_str(), p.source, p.rule), ( - "✻ Run sleep command for 25 seconds", + "Run sleep command for 25 seconds", PreviewSource::Title, None ) @@ -700,9 +706,12 @@ fn codex_title_animations_canonicalize_to_constant_text() { ); // Idle drops the spinner, and foreign titles are not codex's to rewrite. + // Refusing the bare project name is deliberate: for codex the frame is + // the signal, and the text alone says nothing about state. 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, "empty title"); } /// The status row anchors on its parenthetical, not on a literal verb. The @@ -1306,6 +1315,60 @@ fn omp_approval_accepts_every_cursor_preset() { } } +/// The title separator carries the state: `>` idle keeps the label alone, a +/// braille frame folds to `⠋`, `!` stays verbatim, and the feature-disabled +/// `π:` form keeps the label. Label-less idle titles and non-`π` titles +/// refuse: an extension override owns the title and is not omp's to decode. +#[test] +fn omp_title_separators_decode_state_and_label() { + assert_eq!( + OmpSummary.normalize_title("π > Fix the flaky test"), + Some("Fix the flaky test".to_string()) + ); + + // Every working frame must normalize to the same title. + for frame in ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] { + assert_eq!( + OmpSummary.normalize_title(&format!("π {frame} Fix the flaky test")), + Some("⠋ Fix the flaky test".to_string()), + "{frame:?}" + ); + } + assert_eq!( + OmpSummary.normalize_title("π ⠴"), + Some("⠋".to_string()), + "a label-less frame is still the working state" + ); + + // `!` is omp's own waiting-on-you marker, kept verbatim: no prose is + // synthesized outside the approval matchers. + assert_eq!( + OmpSummary.normalize_title("π ! Fix the flaky test"), + Some("! Fix the flaky test".to_string()) + ); + assert_eq!(OmpSummary.normalize_title("π !"), Some("!".to_string())); + + // State feature disabled: `π: {label}`. + assert_eq!( + OmpSummary.normalize_title("π: Fix the flaky test"), + Some("Fix the flaky test".to_string()) + ); + + // Label-less idle shapes carry nothing; everything else is an extension + // override or a foreign program and is refused, never guessed at. + for title in [ + "π", + "π >", + "π: ", + "π >> quoted", + "custom extension title", + "zellij: main", + "", + ] { + assert_eq!(OmpSummary.normalize_title(title), None, "{title:?}"); + } +} + /// ASCII box glyphs do not anchor status: transcript tables and rules use the /// same glyphs. #[test] From 37a6a0f79af8de1d48eabc57baf0f4cd8f06e8c8 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sun, 16 Aug 2026 15:49:34 -0700 Subject: [PATCH 3/6] feat(preview): render an adapter-recognized primary-screen title --- src/preview.rs | 169 ++++++++++++++++++++++++++++++++++++++- src/protocol.rs | 4 +- src/terminal/emulator.rs | 3 - 3 files changed, 171 insertions(+), 5 deletions(-) diff --git a/src/preview.rs b/src/preview.rs index e76eaf4..b5000b7 100644 --- a/src/preview.rs +++ b/src/preview.rs @@ -28,6 +28,11 @@ pub trait ScreenFacts { fn alt_epoch(&self) -> u64; fn alternate_screen(&self) -> bool; fn title(&self) -> Option<&str>; + /// The retained primary-screen title slot: the last sanitized announce + /// made on the primary screen, held across printable output and + /// alternate-screen excursions; an empty announce or a full reset + /// clears it. + fn primary_title(&self) -> Option<&str>; fn live_floor(&self) -> String; /// Every live-viewport row, trailing padding trimmed: the summary /// adapters' structural scan input. @@ -54,6 +59,10 @@ impl ScreenFacts for Emulator { Self::title(self) } + fn primary_title(&self) -> Option<&str> { + Self::primary_title(self) + } + fn live_floor(&self) -> String { Self::live_floor(self) } @@ -92,7 +101,9 @@ pub trait SummaryAdapter: Sync { /// 2. summary adapter: the normalized live status when the CLI's working /// structure is present /// 3. alternate screen: the title while its epoch is current, else the marker -/// 4. primary screen: the live floor +/// 4. primary title: the retained primary-screen announce, only when the +/// adapter affirmatively normalizes it +/// 5. primary screen: the live floor /// /// Tiers 1 and 2 both produce an Anchor. The adapter's model label prefixes /// either status when available. @@ -144,6 +155,23 @@ fn cascade( }, }; } + // Primary-title tier, asymmetric with the alt tier by design: no + // verbatim pass-through. An alt-screen title belongs to the full-screen + // program that owns the display; on the primary screen any inline + // program may have announced a title once and moved on, so only shapes + // an adapter recognizes as live state are safe to render. No adapter, + // or a refusal, falls through to the floor. + if let Some(a) = adapter + && let Some(title) = screen.primary_title() + && let Some(text) = a.normalize_title(title) + { + return Preview { + text, + source: PreviewSource::Title, + rule: None, + frozen: false, + }; + } // An indented status line can remain as the idle floor. Trim only the // display candidate: `live_floor` also feeds teardown-snapshot comparison // and must preserve the emulator row verbatim. @@ -162,6 +190,7 @@ type ResolveKey = ( u64, bool, Option, + Option, Option<(String, &'static str)>, ); @@ -237,6 +266,7 @@ impl PreviewState { screen.alt_epoch(), screen.alternate_screen(), screen.title().map(str::to_owned), + screen.primary_title().map(str::to_owned), blocked.map(|(text, rule)| (text.to_string(), rule)), ); if self.last_key.as_ref() != Some(&key) { @@ -337,6 +367,13 @@ impl PreviewState { } // Finalization excludes registry state because the process has exited. let mut fin = cascade(screen, adapter, None); + if !screen.alternate_screen() && fin.source == PreviewSource::Title { + // The primary title is the registry's sibling: an out-of-band + // live channel, not frozen screen content, and a killed child + // cannot retract a working frame or attention mark. The floor + // is what the frozen screen actually shows. + fin = cascade(screen, None, None); + } fin.frozen = true; self.rendered = fin; } @@ -347,6 +384,7 @@ mod tests { use std::cell::Cell; use super::*; + use crate::harness::summary::{CodexSummary, OmpSummary}; /// Synthetic screen facts with a floor-read counter for the /// revision-gate test. @@ -355,6 +393,9 @@ mod tests { alt_epoch: u64, alt: bool, title: Option, + /// The retained primary-screen announce slot, mirroring + /// `Emulator::primary_title`. + primary_title: Option, floor: String, /// Floor at the last `leave_alt`, mirroring the emulator's /// alt-exit snapshot. @@ -369,6 +410,7 @@ mod tests { alt_epoch: 0, alt: false, title: None, + primary_title: None, floor: floor.into(), alt_leave_floor: None, floor_calls: Cell::new(0), @@ -407,6 +449,11 @@ mod tests { self.floor = f.into(); self.advance(); } + + fn set_primary_title(&mut self, t: &str) { + self.primary_title = Some(t.into()); + self.advance(); + } } impl ScreenFacts for FakeScreen { @@ -426,6 +473,10 @@ mod tests { self.title.as_deref() } + fn primary_title(&self) -> Option<&str> { + self.primary_title.as_deref() + } + fn live_floor(&self) -> String { self.floor_calls.set(self.floor_calls.get() + 1); self.floor.clone() @@ -456,6 +507,12 @@ mod tests { fn model_label(&self, _rows: &[String]) -> Option { self.label.map(str::to_string) } + + /// Recognize every title: rank tests need the primary-title tier + /// eligible without a real normalizer's shape rules. + fn normalize_title(&self, title: &str) -> Option { + Some(title.to_string()) + } } /// The anchor tier outranks the title, carries its rule id, and prepends @@ -639,6 +696,86 @@ mod tests { assert_eq!((p.text.as_str(), p.source), ("", PreviewSource::Floor)); } + /// A retained primary-screen title renders through the adapter's + /// normalizer: omp's `π > {label}` decodes to the bare label. + #[test] + fn a_recognized_primary_title_renders_normalized() { + let now = Instant::now(); + let mut st = PreviewState::new(); + let mut s = FakeScreen::primary("shell"); + s.set_primary_title("π > fix parser"); + let p = st.resolve(now, &s, Some(&OmpSummary), None).clone(); + assert_eq!( + (p.text.as_str(), p.source, p.rule, p.frozen), + ("fix parser", PreviewSource::Title, None, false) + ); + } + + /// A title the adapter refuses falls to the floor: on the primary + /// screen there is no verbatim pass-through. + #[test] + fn a_refused_primary_title_falls_to_the_floor() { + let now = Instant::now(); + let mut st = PreviewState::new(); + let mut s = FakeScreen::primary("shell"); + // codex refuses a bare title: it could be anyone's announce. + s.set_primary_title("fleetcom"); + let p = st.resolve(now, &s, Some(&CodexSummary), None).clone(); + assert_eq!((p.text.as_str(), p.source), ("shell", PreviewSource::Floor)); + } + + /// Without an adapter a retained primary title never renders: any + /// inline program may have announced it. + #[test] + fn a_primary_title_without_an_adapter_falls_to_the_floor() { + let now = Instant::now(); + let mut st = PreviewState::new(); + let mut s = FakeScreen::primary("shell"); + s.set_primary_title("π > fix parser"); + let p = st.resolve(now, &s, None, None).clone(); + assert_eq!((p.text.as_str(), p.source), ("shell", PreviewSource::Floor)); + } + + /// The anchor tier outranks a recognized primary title. + #[test] + fn an_anchor_outranks_the_primary_title() { + let now = Instant::now(); + let mut st = PreviewState::new(); + let mut s = FakeScreen::primary("shell"); + s.set_primary_title("π ⠋ fix parser"); + let adapter = StubAdapter { + live: Some(("Working", "stub:working")), + label: None, + }; + let p = st.resolve(now, &s, Some(&adapter), None).clone(); + assert_eq!( + (p.text.as_str(), p.source, p.rule), + ("Working", PreviewSource::Anchor, Some("stub:working")) + ); + } + + /// A primary-title change alone invalidates the resolve key. + #[test] + fn a_primary_title_change_invalidates_the_key() { + let t0 = Instant::now(); + let mut st = PreviewState::new(); + let mut s = FakeScreen::primary("shell"); + s.set_primary_title("π > one"); + assert_eq!(st.resolve(t0, &s, Some(&OmpSummary), None).text, "one"); + + // The slot changes with no revision bump: only the key's + // primary-title entry can trigger the recompute. + s.primary_title = Some("π > two".into()); + let p = st + .resolve(t0 + TITLE_MIN_HOLD, &s, Some(&OmpSummary), None) + .clone(); + assert_eq!( + (p.text.as_str(), p.source), + ("two", PreviewSource::Title), + "the changed slot must recompute the candidate" + ); + } + /// Rank increases render on the very resolution that observes them. #[test] fn promotion_renders_immediately() { @@ -1084,6 +1221,36 @@ mod tests { ); } + /// A killed child cannot retract its title, so finalization demotes a + /// primary-screen Title to the floor: like the registry, the title is an + /// out-of-band live channel, and freezing "\u{280b} label" would report a + /// dead task as working. The alt-screen Title tier is untouched (the + /// test above pins it). + #[test] + fn finalize_demotes_a_primary_title_to_the_floor() { + let t0 = Instant::now(); + let mut st = PreviewState::new(); + let mut s = FakeScreen::primary("\u{2570}\u{2500} idle box \u{2500}\u{256f}"); + s.set_primary_title("\u{3c0} \u{2819} fix parser"); + let p = st.resolve(t0, &s, Some(&OmpSummary), None).clone(); + assert_eq!( + (p.text.as_str(), p.source), + ("\u{280b} fix parser", PreviewSource::Title), + "premise: the title tier renders while the task lives" + ); + + st.finalize(&s, Some(&OmpSummary)); + let p = st.resolve(t0, &s, Some(&OmpSummary), None).clone(); + assert_eq!( + (p.text.as_str(), p.source, p.frozen), + ( + "\u{2570}\u{2500} idle box \u{2500}\u{256f}", + PreviewSource::Floor, + true + ) + ); + } + /// Floor previews remove leading layout indentation. #[test] fn floor_preview_trims_leading_indentation() { diff --git a/src/protocol.rs b/src/protocol.rs index 376bdec..2002143 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -274,7 +274,9 @@ pub enum PreviewSource { Floor, /// The alternate screen is active with no usable title. Marker, - /// The child's window title, honored only on the alternate screen. + /// The child's window title. On the alternate screen any captured title + /// qualifies; on the primary screen only a retained announce the + /// summary adapter affirmatively normalizes. Title, /// The cascade's top tier: a normalized screen or registry status. Anchor, diff --git a/src/terminal/emulator.rs b/src/terminal/emulator.rs index eb027c0..715eb9a 100644 --- a/src/terminal/emulator.rs +++ b/src/terminal/emulator.rs @@ -545,9 +545,6 @@ impl Emulator { /// announce or a full reset clears it. [`Emulator::title`] serves the /// staged/epoch view; this slot serves inline programs whose constant /// repaints disclaim staging. - // Unused pub in a bin crate is dead code; the preview resolution layer - // consumes this once its phase lands. - #[allow(dead_code)] pub fn primary_title(&self) -> Option<&str> { self.alt.primary_title.as_deref() } From d4c5b3b6069a6beee3d867bed464f068bf09c6f5 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sun, 16 Aug 2026 15:56:55 -0700 Subject: [PATCH 4/6] test(corpus): pin the omp title tier; align docs and README scenes --- docs/commands.md | 2 +- src/app_readme_tests.rs | 6 ++-- src/harness/summary_tests.rs | 21 ++++++++++++- tests/corpus/README.md | 6 +++- tests/corpus/preview_omp_idle_titled.bin | 39 ++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 tests/corpus/preview_omp_idle_titled.bin diff --git a/docs/commands.md b/docs/commands.md index e30d40a..b77186b 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -202,7 +202,7 @@ The box uses a two-column layout. When height is limited, group headers drop fir A centered box over the dashboard showing the selected task's live screen (the last screenful). `↑`/`↓` (or `k`/`j`) switch which task you're peeking at; `Enter` attaches to it; `r` reruns it if it has finished; `Space`, `Esc`, or `q` closes. -The footer's `preview:` segment names the source of the row's dashboard preview: `floor` (the last non-blank row of the live screen), `marker` (a full-screen program with no usable title), `title` (the child's window title), or `anchor/` (a recognized agent status, tagged with the matcher that produced it). Most rules name a screen matcher, such as `claude:spinner` or `codex:approval-menu`. The `claude:registry-approval` and `claude:registry-waiting` rules instead come from Claude's on-disk session status. +The footer's `preview:` segment names the source of the row's dashboard preview: `floor` (the last non-blank row of the live screen), `marker` (a full-screen program with no usable title), `title` (the child's window title; a full-screen program's title renders verbatim, while on the primary screen only a title the task's agent adapter recognizes renders — a refusal falls to `floor`), or `anchor/` (a recognized agent status, tagged with the matcher that produced it). Most rules name a screen matcher, such as `claude:spinner` or `codex:approval-menu`. The `claude:registry-approval` and `claude:registry-waiting` rules instead come from Claude's on-disk session status. ## Attached diff --git a/src/app_readme_tests.rs b/src/app_readme_tests.rs index 01617dc..fac73d6 100644 --- a/src/app_readme_tests.rs +++ b/src/app_readme_tests.rs @@ -129,7 +129,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { lifecycle: Lifecycle::Active, parked: false, preview: anchor( - "✻ Scope small fixes for dashboard and CLI", + "Scope small fixes for dashboard and CLI", "claude:action-row", ), started_ago: mins(2), @@ -355,7 +355,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { name: None, lifecycle: Lifecycle::Active, parked: false, - preview: anchor("✻ Review GitHub issue 780", "claude:action-row"), + preview: anchor("Review GitHub issue 780", "claude:action-row"), started_ago: mins(6), quiet_ago: Some(secs(2)), finished_ago: None, @@ -548,7 +548,7 @@ const FLEETCOM_CLIPPY: &str = const LOGRIA_WATCH: &str = "[Running 'cargo test'] test result: ok. 223 passed; 0 failed"; const LOGRIA_DOC: &str = "Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.41s"; /// Quiet-fixture summary preview. -const SUMMARY_QUIET: &str = "✻ Review fleetcom preview design document"; +const SUMMARY_QUIET: &str = "Review fleetcom preview design document"; /// Visible `cargo test` tail used by the peek fixture. fn cargo_test_screen(id: u64) -> ScreenView { diff --git a/src/harness/summary_tests.rs b/src/harness/summary_tests.rs index 2af0c14..5c34280 100644 --- a/src/harness/summary_tests.rs +++ b/src/harness/summary_tests.rs @@ -47,6 +47,11 @@ fn floor(text: &str) -> (String, PreviewSource, Option<&'static str>) { (text.to_string(), PreviewSource::Floor, None) } +/// Expected title-preview tuple: an adapter-normalized retained title. +fn titled(text: &str) -> (String, PreviewSource, Option<&'static str>) { + (text.to_string(), PreviewSource::Title, None) +} + /// Selection is a basename match on the first word only: wider than /// harness detection (arguments are tolerated), but env prefixes and /// shell syntax glued to the word select nothing. @@ -1551,7 +1556,8 @@ fn corpus_idle_states_fall_through() { } } -/// omp is an inline UI: an idle screen falls through to the floor tier — +/// Negative control for the titled fixture: this rows-only capture retains +/// no title announce, so the idle screen falls through to the floor tier — /// its input row — never the alternate-screen marker the other CLIs reach. #[test] fn corpus_omp_idle_falls_through_to_the_floor() { @@ -1563,6 +1569,19 @@ fn corpus_omp_idle_falls_through_to_the_floor() { assert_eq!(got, floor(&format!("╰─{}─╯", " ".repeat(116)))); } +/// The same idle screen behind a retained `π >