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);