Skip to content

Commit 6abbf7f

Browse files
zoza1982claude
andauthored
fix(tui): theme the AI plan popup's highlight, help, and pending markers (#169)
* fix(tui): theme the AI plan popup's highlight, help, and pending markers The AI plan overlay already filled its interior with the theme background, but the selected step was drawn with a hardcoded magenta bar and the help line and pending-step markers with a hardcoded gray — jarring on non-default themes (e.g. mc's blue). The highlighted step now uses the theme's selection colors (like the panes), and the help + pending markers use the theme's status color. Approval/run status keeps its semantic colors (green/red/cyan). Test asserts the overlay carries the theme background, the highlighted step uses the theme selection colour, and no hardcoded magenta highlight remains. Snapshots are unaffected (they capture glyphs, not colour). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(tui): assert the AI overlay's help/pending markers use theme.status Review follow-up: the theming test only covered the highlight color. Override theme.status to a distinctive value and assert both a pending step's marker and the help line render with it, so the help/pending half of the fix has regression protection too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e3f0ffd commit 6abbf7f

2 files changed

Lines changed: 97 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
182182

183183
### Fixed
184184

185+
- **The AI plan popup now matches the active theme.** The overlay already used the theme background,
186+
but the selected step was drawn with a hardcoded magenta bar and the help/pending markers with a
187+
hardcoded gray — jarring on non-default themes. The highlighted step now uses the theme's selection
188+
colors (like the panes), and the help line and pending markers use the theme's status color.
189+
Approval/run status keeps its semantic colors (green approved, red rejected/failed, cyan done).
190+
185191
- **A large copy/move no longer stalls on the pre-flight size scan.** The scan that sizes the source
186192
tree for a percentage/ETA is now **time-boxed** (≤ 0.8 s) and **entry-capped** — so copying a huge
187193
or remote (SFTP) folder starts almost immediately with an indeterminate, animated bar (MC-style)

crates/cairn-tui/src/render.rs

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,8 +1333,11 @@ fn render_ai_plan(frame: &mut Frame, plan: &Plan, cursor: usize, theme: &Theme)
13331333
.steps
13341334
.iter()
13351335
.map(|s| {
1336+
// Approval/run status keeps its semantic colors (green approved, red rejected/failed,
1337+
// cyan done); a still-pending step uses the theme's status color so it doesn't clash on a
1338+
// non-default theme (it was a hardcoded gray).
13361339
let (marker, color) = match s.status {
1337-
StepStatus::Pending => ('·', Color::Gray),
1340+
StepStatus::Pending => ('·', theme.status),
13381341
StepStatus::Approved => ('✓', Color::Green),
13391342
StepStatus::Rejected => ('✗', Color::Red),
13401343
StepStatus::Done => ('●', Color::Cyan),
@@ -1354,7 +1357,13 @@ fn render_ai_plan(frame: &mut Frame, plan: &Plan, cursor: usize, theme: &Theme)
13541357
})
13551358
.collect();
13561359
let list = List::new(items)
1357-
.highlight_style(Style::default().bg(Color::Magenta).fg(Color::Black))
1360+
// Use the theme's selection colors (like the panes) instead of a hardcoded magenta bar, so
1361+
// the highlighted step matches the active theme.
1362+
.highlight_style(
1363+
Style::default()
1364+
.bg(theme.selection_bg)
1365+
.fg(theme.selection_fg),
1366+
)
13581367
.highlight_symbol("▶ ");
13591368
let mut list_state = ListState::default();
13601369
if !plan.steps.is_empty() {
@@ -1368,7 +1377,7 @@ fn render_ai_plan(frame: &mut Frame, plan: &Plan, cursor: usize, theme: &Theme)
13681377
"↵ approve · x reject · esc abort · no bulk (irreversible)"
13691378
};
13701379
frame.render_widget(
1371-
Paragraph::new(Line::from(help)).style(Style::default().fg(Color::Gray)),
1380+
Paragraph::new(Line::from(help)).style(Style::default().fg(theme.status)),
13721381
help_area,
13731382
);
13741383
}
@@ -2341,6 +2350,85 @@ mod tests {
23412350
assert!(changed, "themed background must paint at least one cell");
23422351
}
23432352

2353+
#[test]
2354+
fn ai_plan_overlay_matches_the_theme() {
2355+
use cairn_ai::{capability_for, Plan, PlanState, PlanStep, StepStatus};
2356+
let steps = ["list", "copy"]
2357+
.iter()
2358+
.filter_map(|t| {
2359+
Some(PlanStep {
2360+
tool: (*t).to_owned(),
2361+
input: serde_json::Value::Null,
2362+
description: format!("{t} the things"),
2363+
capability: capability_for(t)?,
2364+
status: StepStatus::Pending,
2365+
error: None,
2366+
output: None,
2367+
})
2368+
})
2369+
.collect();
2370+
let plan = Plan {
2371+
summary: "s".to_owned(),
2372+
steps,
2373+
state: PlanState::Proposed,
2374+
};
2375+
let mut s = ready_state();
2376+
s.overlay = Some(cairn_core::Overlay::AiPlan { plan, cursor: 0 });
2377+
// Distinctive theme colors so each role is distinguishable from the DARK defaults.
2378+
let status = Color::Rgb(1, 2, 3);
2379+
let theme = Theme {
2380+
background: Some(Color::Blue),
2381+
selection_bg: Color::Green,
2382+
selection_fg: Color::Black,
2383+
status,
2384+
..Theme::DARK
2385+
};
2386+
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
2387+
terminal.draw(|f| render(f, &s, &theme)).unwrap();
2388+
let buf = terminal.backend().buffer().clone();
2389+
let width = usize::from(buf.area().width);
2390+
let mut saw_title_bg = false;
2391+
let mut highlight_is_theme = false;
2392+
let mut pending_is_status = false;
2393+
let mut help_is_status = false;
2394+
let mut any_magenta = false;
2395+
for row in buf.content().chunks(width) {
2396+
let text: String = row.iter().map(|c| c.symbol()).collect();
2397+
if text.contains("AI plan") {
2398+
saw_title_bg = row.iter().any(|c| c.style().bg == Some(Color::Blue));
2399+
}
2400+
if text.contains("list the things") {
2401+
// The highlighted step must use the theme selection bg, never a hardcoded magenta.
2402+
highlight_is_theme = row.iter().any(|c| c.style().bg == Some(Color::Green));
2403+
}
2404+
if text.contains("copy the things") {
2405+
// A non-highlighted pending step's marker uses the theme status color (was gray).
2406+
pending_is_status = row.iter().any(|c| c.style().fg == Some(status));
2407+
}
2408+
if text.contains("approve") {
2409+
// The help line uses the theme status color (was gray).
2410+
help_is_status = row.iter().any(|c| c.style().fg == Some(status));
2411+
}
2412+
if row.iter().any(|c| c.style().bg == Some(Color::Magenta)) {
2413+
any_magenta = true;
2414+
}
2415+
}
2416+
assert!(
2417+
saw_title_bg,
2418+
"the AI overlay's interior carries the theme background"
2419+
);
2420+
assert!(
2421+
highlight_is_theme,
2422+
"the highlighted step uses the theme selection colour"
2423+
);
2424+
assert!(
2425+
pending_is_status,
2426+
"a pending step's marker uses the theme status colour"
2427+
);
2428+
assert!(help_is_status, "the help line uses the theme status colour");
2429+
assert!(!any_magenta, "no hardcoded magenta highlight remains");
2430+
}
2431+
23442432
#[test]
23452433
fn overlays_pick_up_the_theme_background() {
23462434
// A dialog must match the active theme, not the terminal default that its `Clear` would leave.

0 commit comments

Comments
 (0)