fix(tui): clear modal backdrop centrally

Paint an opaque modal backdrop from ViewStack before rendering overlays so stale transcript glyphs cannot bleed through older or minimal modal views. Keep the existing popup-surface behavior and document the broader #3732 coverage in both changelogs.

Verification:

- cargo fmt --all --check

- git diff --check

- CARGO_TARGET_DIR=/Volumes/VIXinSSD/codewhale-target cargo test -p codewhale-tui --bin codewhale-tui --locked view_stack_paints_opaque_backdrop_before_modal -- --nocapture

- CARGO_TARGET_DIR=/Volumes/VIXinSSD/codewhale-target cargo test -p codewhale-tui --bin codewhale-tui --locked plan_prompt_paints_an_opaque_modal_surface -- --nocapture

- CARGO_TARGET_DIR=/Volumes/VIXinSSD/codewhale-target cargo test -p codewhale-tui --bin codewhale-tui --locked plan_prompt_footer_stays_compact_on_narrow_terminals -- --nocapture

- CARGO_TARGET_DIR=/Volumes/VIXinSSD/codewhale-target cargo test -p codewhale-tui --bin codewhale-tui --locked tui::views::tests:: -- --nocapture

Refs #3732
This commit is contained in:
Hunter B committed 2026-06-28 15:14:03 -07:00
1 parent 4cddca0d30
commit 4204029cf8
3 files changed
+87 -7

No files matched your search

+3 -3
View File
@@ -68,9 +68,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed runtime API approval handling so workspace trust no longer auto-resolves
ordinary tool approvals; trust now only participates in full-access retry
decisions while YOLO/auto-approve remains the approval bypass (#3736).
- Fixed Plan/request-input modal surfaces so popup interiors are painted
opaquely, and compacted the Plan confirmation footer so action choices stay
visible on narrow terminals instead of truncating (#3732).
- Fixed modal surfaces so the shared view stack paints an opaque backdrop before
any overlay, while Plan/request-input popup interiors stay opaque and the Plan
confirmation footer keeps action choices visible on narrow terminals (#3732).
- Added a turn-loop Plan-mode guard for file-writing tools and write-capable MCP
tools so Plan's "no writes" promise is enforced before approval or execution,
not only by the sandbox/catalog layer (#3734).
+3 -3
View File
@@ -68,9 +68,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed runtime API approval handling so workspace trust no longer auto-resolves
ordinary tool approvals; trust now only participates in full-access retry
decisions while YOLO/auto-approve remains the approval bypass (#3736).
- Fixed Plan/request-input modal surfaces so popup interiors are painted
opaquely, and compacted the Plan confirmation footer so action choices stay
visible on narrow terminals instead of truncating (#3732).
- Fixed modal surfaces so the shared view stack paints an opaque backdrop before
any overlay, while Plan/request-input popup interiors stay opaque and the Plan
confirmation footer keeps action choices visible on narrow terminals (#3732).
- Added a turn-loop Plan-mode guard for file-writing tools and write-capable MCP
tools so Plan's "no writes" promise is enforced before approval or execution,
not only by the sandbox/catalog layer (#3734).
+81 -1
View File
@@ -86,6 +86,16 @@ pub(crate) fn render_modal_surface(area: Rect, popup_area: Rect, buf: &mut Buffe
.render(popup_area, buf);
}
fn render_modal_backdrop(area: Rect, buf: &mut Buffer) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
buf[(x, y)]
.set_symbol(" ")
.set_style(Style::default().bg(palette::DEEPSEEK_INK));
}
}
}
#[derive(Debug, Clone)]
pub enum CommandPaletteAction {
ExecuteCommand { command: String },
@@ -350,6 +360,9 @@ impl ViewStack {
}
pub fn render(&self, area: Rect, buf: &mut Buffer) {
if !self.views.is_empty() {
render_modal_backdrop(area, buf);
}
for view in &self.views {
view.render(area, buf);
}
@@ -2387,7 +2400,11 @@ mod tests {
use crossterm::event::{
KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use ratatui::{buffer::Buffer, layout::Rect};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
};
use std::borrow::Cow;
use std::ffi::OsString;
use std::fs;
@@ -3319,6 +3336,69 @@ base_url = "https://api.xiaomimimo.com/v1"
assert_eq!(stack.top_kind(), Some(ModalKind::Help));
}
struct BareModal;
impl ModalView for BareModal {
fn kind(&self) -> ModalKind {
ModalKind::ContextMenu
}
fn handle_key(&mut self, _key: KeyEvent) -> ViewAction {
ViewAction::None
}
fn render(&self, area: Rect, buf: &mut Buffer) {
let x = area.x + area.width / 2;
let y = area.y + area.height / 2;
buf[(x, y)]
.set_symbol("M")
.set_style(Style::default().fg(Color::White).bg(Color::Red));
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
#[test]
fn view_stack_paints_opaque_backdrop_before_modal() {
let area = Rect::new(0, 0, 24, 8);
let modal_x = area.x + area.width / 2;
let modal_y = area.y + area.height / 2;
let mut buf = Buffer::empty(area);
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
buf[(x, y)]
.set_symbol("X")
.set_style(Style::default().fg(Color::Red).bg(Color::Blue));
}
}
let mut stack = ViewStack::new();
stack.push(BareModal);
stack.render(area, &mut buf);
assert_eq!(buf[(modal_x, modal_y)].symbol(), "M");
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
if x == modal_x && y == modal_y {
continue;
}
let cell = &buf[(x, y)];
assert_eq!(
cell.symbol(),
" ",
"stale glyph at ({x},{y}) must be cleared"
);
assert_eq!(
cell.bg,
palette::DEEPSEEK_INK,
"backdrop at ({x},{y}) must be opaque"
);
}
}
}
fn buffer_text(buf: &Buffer, area: Rect) -> String {
let mut out = String::new();
for y in area.top()..area.bottom() {