feat(approval): collapse approval modal to a one-line banner with Tab

Previously the approval modal rendered as a full-screen takeover
card that hid the transcript behind it, so users had to dismiss the
prompt — losing the decision context — just to re-read the tool
call they were being asked to approve. The new collapsed mode flips
the modal to a single-line banner pinned at the bottom of the area
("<tool> — <risk badge>  [Tab to expand]"), so the conversation
stays visible while the decision is pending. Tab toggles between
the two modes; the selected option, pending-confirm state, and
risk colour scheme are preserved across the toggle.

Test pin: a fresh `ApprovalView` starts expanded, the first Tab
collapses, the second Tab restores — and both return `ViewAction::None`
so no decision side effect leaks out of the toggle.

Harvested from PR #1455 by @tiger-dog

Note for the maintainer: PR #1455 also includes a Chinese-language
preamble to `prompts/base.md` that biases reasoning_content toward
Chinese on Chinese-language turns. That change touches the system
prompt and is left for a separate sign-off — see
.private/handoffs/v0.8.32-1455-prompt-preamble.md for the diff and
the suggested call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-05-12 00:29:31 -05:00
parent 02f889f193
commit a57cb4dfa7
3 changed files with 63 additions and 0 deletions
+9
View File
@@ -16,6 +16,15 @@ real world uses."
### Fixed
- **Approval modal can be collapsed to a one-line banner with
Tab** (harvested from PR #1455 by **@tiger-dog**). Previously the
approval prompt rendered as a full-screen takeover that hid the
transcript behind it, so users had to dismiss the modal just to
remember which tool call they were being asked to approve. Tab
now toggles between the takeover card and a single-line bottom
banner — the rest of the conversation stays visible while the
decision is pending. Tab again restores the full card; the
selection state is preserved across the toggle.
- **Markdown renderer no longer eats underscores inside
identifiers** (harvested from PR #1455 by **@tiger-dog**). The
inline parser was matching `_italic_` against the underscore in
+29
View File
@@ -504,6 +504,8 @@ pub struct ApprovalView {
pending_confirm: Option<ApprovalOption>,
timeout: Option<Duration>,
requested_at: Instant,
/// Whether the approval card is collapsed to a single-line banner.
pub(crate) collapsed: bool,
}
impl ApprovalView {
@@ -520,6 +522,7 @@ impl ApprovalView {
pending_confirm: None,
timeout: None,
requested_at: Instant::now(),
collapsed: false,
}
}
@@ -625,6 +628,10 @@ impl ModalView for ApprovalView {
fn handle_key(&mut self, key: KeyEvent) -> ViewAction {
match key.code {
KeyCode::Tab => {
self.collapsed = !self.collapsed;
ViewAction::None
}
KeyCode::Up | KeyCode::Char('k') => {
self.select_prev();
ViewAction::None
@@ -1153,6 +1160,28 @@ mod tests {
assert_eq!(view.risk(), RiskLevel::Benign);
}
#[test]
fn tab_toggles_collapsed_card_so_transcript_stays_visible() {
// Regression for PR #1455 / @tiger-dog: the approval modal
// rendered as a full-screen takeover that hid the transcript
// behind it, so users had to dismiss the prompt to remember
// what they were approving. Tab now flips between the full
// takeover card and a single-line bottom banner.
let mut view = ApprovalView::new(benign_request());
assert!(
!view.collapsed,
"modal must start expanded so first-time users notice it"
);
let action = view.handle_key(create_key_event(KeyCode::Tab));
assert!(matches!(action, ViewAction::None));
assert!(view.collapsed, "first Tab collapses the card");
let action = view.handle_key(create_key_event(KeyCode::Tab));
assert!(matches!(action, ViewAction::None));
assert!(!view.collapsed, "second Tab restores the takeover card");
}
#[test]
fn test_approval_view_navigation() {
let mut view = ApprovalView::new(benign_request());
+25
View File
@@ -1010,6 +1010,31 @@ impl Renderable for ApprovalWidget<'_> {
return;
}
// Collapsed mode: a single-line banner at the bottom of the area
// so the user can still see the transcript behind it.
if self.view.collapsed {
let bar_y = area.y.saturating_add(area.height.saturating_sub(1));
let bar_area = Rect::new(area.x, bar_y, area.width, 1);
Clear.render(bar_area, buf);
let risk = self.request.risk;
let palette_colors = approval_palette(risk);
let summary = format!(
" {}{} [Tab to expand] ",
self.request.tool_name,
risk_badge_text(risk, self.view.locale()),
);
let line = Line::from(Span::styled(
summary,
Style::default()
.fg(palette::DEEPSEEK_INK)
.bg(palette_colors.accent)
.add_modifier(Modifier::BOLD),
));
Paragraph::new(line).render(bar_area, buf);
return;
}
let card_area = compute_takeover_area(area);
Clear.render(card_area, buf);