From a57cb4dfa7ff2d92a180e5d4e6365e901bdebe6f Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Tue, 12 May 2026 00:29:31 -0500 Subject: [PATCH] feat(approval): collapse approval modal to a one-line banner with Tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 (" [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) --- CHANGELOG.md | 9 +++++++++ crates/tui/src/tui/approval.rs | 29 +++++++++++++++++++++++++++++ crates/tui/src/tui/widgets/mod.rs | 25 +++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a920dab4..c88894f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/crates/tui/src/tui/approval.rs b/crates/tui/src/tui/approval.rs index 870e3bbe..32f90919 100644 --- a/crates/tui/src/tui/approval.rs +++ b/crates/tui/src/tui/approval.rs @@ -504,6 +504,8 @@ pub struct ApprovalView { pending_confirm: Option, timeout: Option, 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()); diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index f1852980..da7bb9d6 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -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);