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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 54 additions & 9 deletions src/harness/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
//! 2. Return `None` when the expected structure is absent or inconsistent.
//! 3. Preserve CLI-generated ellipsis truncation.
//!
//! Remove spinner glyphs, elapsed counters, throughput data, and key hints during
//! normalization; preserve the CLI's status text. The only synthesized status is
//! `awaiting approval`, for approval menus (claude's dialog and codex's modal) and
//! for omp's `!` title, which is that harness's only blocked signal. Supported
//! screen structures are recorded in the corpus fixtures in `tests/corpus`.
//! During normalization, omit spinner glyphs, elapsed counters, throughput data,
//! and key hints; preserve the CLI's status text. Synthesize `awaiting approval`
//! only for approval menus (Claude's dialog and Codex's modal), omp's `!` title
//! (its only blocked signal), and grok's blocked titles (`⚠ Action Required`,
//! `Running: Plan: Exit`, `Running: Ask: …`). For grok's blocked states, normalize
//! the announced title; no structural match exists for them. See `tests/corpus`
//! for supported screen structures.

use crate::preview::SummaryAdapter;

Expand Down Expand Up @@ -656,10 +658,14 @@ fn codex_working(header: &str, after_paren: &str) -> String {

// ------------------------------------------------------------------ grok --

/// grok (alt screen). The pin is its bordered input box; the status row
/// (braille spinner while working, `Worked for {n}s` after a turn, or
/// `◎ … still running` / `◎ waiting` while background work is live) is
/// the first painted row above the box's top border.
/// 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`].
pub struct GrokSummary;

impl SummaryAdapter for GrokSummary {
Expand All @@ -686,6 +692,45 @@ impl SummaryAdapter for GrokSummary {
let (_, bottom) = grok_input_box(rows)?;
grok_border_label(&rows[bottom])
}

/// Parse grok 1.0.50's raw `SetTitle` announcements, unchanged by theme
/// processing. Observed forms:
/// - Splash: `grok`.
/// - Idle: `{summary} - grok`.
/// - Working before the session is named: `{frame} - {status} - grok`.
/// - Working after the session is named: `{frame} - {status} - {summary} - grok`.
/// - Permission prompt: `⚠ Action Required - {frame} - {status}… - grok`.
///
/// Use the single braille character as the frame. Treat the status as grok's
/// live phrase, truncated with its own `…` at roughly 40 columns, and the
/// summary as the session name.
///
/// Map the three blocked states to [`AWAITING_APPROVAL`]: match
/// `⚠ Action Required` for permission prompts, `Running: Plan: Exit` for plan
/// approval, and the `Running: Ask: ` prefix for questions. For other working
/// titles, replace the frame with `⠋` and keep the first status segment. For
/// idle titles, return the summary. Treat ` - ` as the status/session
/// delimiter. Keep only the first segment after the frame as the status and
/// discard the remainder, even if the status contains that delimiter.
///
/// Return `None` for a title without the ` - grok` suffix, bare `grok`, or an
/// empty status or summary. On grok's alternate screen, render the captured
/// title verbatim when normalization returns `None`.
fn normalize_title(&self, title: &str) -> Option<String> {
let rest = title.strip_suffix(" - grok")?;
if rest.starts_with("⚠ Action Required") {
return Some(AWAITING_APPROVAL.to_string());
}
let Some(working) = after_frame(rest, braille_frame).and_then(|r| r.strip_prefix("- "))
else {
return (!rest.is_empty()).then(|| rest.to_string());
};
let status = working.find(" - ").map_or(working, |i| &working[..i]);
if status == "Running: Plan: Exit" || status.starts_with("Running: Ask: ") {
return Some(AWAITING_APPROVAL.to_string());
}
(!status.is_empty()).then(|| format!("⠋ {status}"))
}
}

/// grok's input box: the bottom-most `╰…╯` border (the splash panel's box
Expand Down
103 changes: 100 additions & 3 deletions src/harness/summary_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,75 @@ fn grok_still_running_shapes() {
);
}

/// Test each observed ` - `-joined title form from grok 1.0.50. Expect the
/// session name for idle titles, a normalized frame and first status segment for
/// working titles, and the blocked status for the three blocked forms. Return
/// `None` for bare `grok` and titles without the suffix.
#[test]
fn grok_title_segments_decode_state_and_status() {
let t = |title: &str| GrokSummary.normalize_title(title);

assert_eq!(
t("Run ls -la list directory file names - grok"),
Some("Run ls -la list directory file names".to_string())
);

// Exercise every working frame with and without a session name; expect the
// same normalized title for both forms.
for frame in BRAILLE_FRAMES {
assert_eq!(
t(&format!("{frame} - Waiting for response… - grok")),
Some("⠋ Waiting for response…".to_string()),
"{frame:?}"
);
assert_eq!(
t(&format!(
"{frame} - Thinking - Run ls -la list directory file names - grok"
)),
Some("⠋ Thinking".to_string()),
"{frame:?}"
);
}
// Treat statuses without grok's trailing `…`, including `Not responding`,
// as ordinary working states.
assert_eq!(
t("⠼ - Running: verbose|clap|arg\\( - Add verbose flag to main.rs - grok"),
Some("⠋ Running: verbose|clap|arg\\(".to_string())
);
assert_eq!(
t("⠋ - Not responding - grok"),
Some("⠋ Not responding".to_string())
);
// For statuses containing ` - `, keep only the first segment.
assert_eq!(
t("⠙ - Running: Edit a - b.rs - Add verbose flag to main.rs - grok"),
Some("⠋ Running: Edit a".to_string())
);

// Identify permission prompts by the prefix; identify plan approval and
// questions from their status strings.
for title in [
"⚠ Action Required - ⠙ - Create junk/a.txt then remove the direct… - grok",
"⚠ Action Required - grok",
"⠹ - Running: Plan: Exit - Add verbose flag to main.rs - grok",
"⠼ - Running: Ask: Which greeting do you pre… - Ask preferred greeting hello versus hi - grok",
] {
assert_eq!(t(title), Some(AWAITING_APPROVAL.to_string()), "{title:?}");
}

// Return `None` for bare, foreign, suffix-less, and empty-status titles.
// On the alternate screen, use that result to display the captured title.
for title in [
"grok",
"⠋ - Thinking",
"⠋ - - grok",
"π > fix the parser",
"",
] {
assert_eq!(t(title), None, "{title:?}");
}
}

/// omp title normalization strips idle and disabled-state prefixes, folds
/// working frames to `⠋`, maps `!` to the blocked status, and rejects
/// unsupported shapes.
Expand Down Expand Up @@ -1545,9 +1614,10 @@ fn corpus_positive_states_resolve_exactly() {
"grok:still-running",
),
];
// Title-tier positives: a retained primary title the adapter recognizes,
// on a screen that anchors nothing.
let titles: [(&str, &[u8], &dyn SummaryAdapter, &str); 2] = [
// Exercise both title paths against screens without structural anchors:
// retained OSC titles on the primary screen and live announces on the
// alternate screen.
let titles: [(&str, &[u8], &dyn SummaryAdapter, &str); 6] = [
(
"preview_omp_idle_titled",
include_bytes!("../../tests/corpus/preview_omp_idle_titled.bin"),
Expand All @@ -1560,6 +1630,33 @@ fn corpus_positive_states_resolve_exactly() {
&OmpSummary,
"⠋ List files in current directory",
),
(
"preview_grok_idle_titled",
include_bytes!("../../tests/corpus/preview_grok_idle_titled.bin"),
&GrokSummary,
"Run ls -la list directory file names",
),
// Expect a `┃`-fenced option list instead of the input box.
(
"preview_grok_permission",
include_bytes!("../../tests/corpus/preview_grok_permission.bin"),
&GrokSummary,
AWAITING_APPROVAL,
),
// Exercise the title tier with the input box present and
// `◆ Waiting on plan approval` in the probe row.
(
"preview_grok_plan_approval",
include_bytes!("../../tests/corpus/preview_grok_plan_approval.bin"),
&GrokSummary,
AWAITING_APPROVAL,
),
(
"preview_grok_question",
include_bytes!("../../tests/corpus/preview_grok_question.bin"),
&GrokSummary,
AWAITING_APPROVAL,
),
];
// Fixture names start with the program word: require a positive case for
// every registered harness, in whichever tier carries its state.
Expand Down
9 changes: 8 additions & 1 deletion tests/corpus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ structural tier, since its chrome is user-themeable, so these fixtures pin that
nothing anchors on them and that the title tier carries omp's state. The titled
fixtures reuse their untitled rows verbatim behind a title announce: `ESC]0;π >
fix the parser BEL` for idle and `ESC]0;π ⠇ List files in current directory
BEL` for working.
BEL` for working. Capture grok 1.0.50's title announce immediately after entry
into the alternate screen. For these blocked states, title normalization is the
only matching path. Use a same-width synthetic value for the header path to
prevent a byte-for-byte match with the captured header.

| Fixture | Scenario | Coverage |
| --- | --- | --- |
Expand Down Expand Up @@ -90,6 +93,10 @@ BEL` for working.
| `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_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 |
| `preview_grok_permission.bin` | grok 1.0.50 permission prompt: `┃`-fenced option list in place of the input box; `⚠ Action Required - ⠸ - … - grok` title announce | Match the `⚠ Action Required` prefix and return `awaiting approval`; this fixture has no input-box anchor |
| `preview_grok_plan_approval.bin` | grok 1.0.50 plan approval: `plan.md` panel, `◆ Waiting on plan approval` above the input box, `plan approval` border label, `⠦ - Running: Plan: Exit - … - grok` title announce | The `◆` probe row has no structural match; return `awaiting approval` from the title tier |
| `preview_grok_question.bin` | grok 1.0.50 Ask tool: `┃`-fenced answer list in place of the input box; `⠼ - Running: Ask: … - grok` title announce | Match the `Running: Ask:` prefix and return `awaiting approval`; this fixture has no input-box anchor |
| `preview_omp_idle.bin` | omp 18.8.7 idle: logo, tip, `π >` status band, `╰─` composer gutter | negative: no anchor on the band layout; fall-through to the floor tier, no marker for an inline UI |
| `preview_omp_idle_titled.bin` | `preview_omp_idle.bin` after a `π > fix the parser` title announce | idle prefix stripped and `fix the parser` rendered in the primary-screen title tier |
| `preview_omp_working.bin` | omp 18.8.7 mid tool call: `⎋ List files in current directory` working row, `⠇ 4s >` status band, composer gutter | negative: no anchor on the working row or band; fall-through to the floor tier |
Expand Down
39 changes: 39 additions & 0 deletions tests/corpus/preview_grok_idle_titled.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[?1049h]0;Run ls -la list directory file names - grok
~/Projects/fixture-sources 3.0K / 500K
































╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ❯ │
╰────────────────────────────────────────────────────────────────────────────── Grok 4.5 (xhigh) · always-approve ─╯

Shift+Tab:mode │ Ctrl+x:shortcuts
39 changes: 39 additions & 0 deletions tests/corpus/preview_grok_permission.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[?1049h]0;⚠ Action Required - ⠸ - Create junk/a.txt then remove the direct… - Create junk dir with a.txt then rm -rf - grok
~/Projects/fixture-sources/grok 19K / 256K │ [Dashboard]


❯ Create a directory named junk containing a file named a.txt, then remove the whole directory with 1:16 PM
rm -rf junk.


◆ Thought for 2.3s

I'll create junk/a.txt, then remove the directory with rm -rf junk. 1:16 PM

◆ Running create junk/a.txt then remove the directory













◆ Create junk/a.txt then remove the direct… 10.0s 14s ⇣19.3k [↓][stop]

┃
┃ Create junk/a.txt then remove the directory
┃ mkdir -p junk && printf '' > junk/a.txt && ls -la junk && rm -rf junk && if [ -e junk ];
┃ then echo "STILL_EXISTS"; else echo "REMOVED"; fi
┃
┃ 1 (●) Yes, and don't ask again for anything (always-approve mode)
┃ 2 (○) Yes, proceed
┃ 3 (○) No, reject (type to add feedback)
┃

1/3:select │ Tab:next option │ Ctrl+o:always-approve │ Ctrl+c:cancel │ Esc:scrollback
Loading
Loading