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:
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user