Skip to content

feat(tui): enhance approval modal button prominence with visual grouping#3437

Open
donglovejava wants to merge 5 commits into
Hmbown:mainfrom
donglovejava:fix/approval-modal-button-prominence
Open

feat(tui): enhance approval modal button prominence with visual grouping#3437
donglovejava wants to merge 5 commits into
Hmbown:mainfrom
donglovejava:fix/approval-modal-button-prominence

Conversation

@donglovejava

Copy link
Copy Markdown
Contributor

Summary

This PR enhances the approval modal UX by making the approve/deny buttons more visually prominent and easier to distinguish.

Problem

The approval modal displayed all four options (Approve once, Approve always, Deny, Abort) as a flat list with no visual grouping. Users had to read each line carefully to distinguish between approve and deny actions. The selected option was only indicated by a color change, which could be missed at a glance.

Solution

Visual Grouping

  • Added a separator line between the approve group (options 0-1) and the deny/abort group (options 2-3), making the decision surface read as two distinct choice clusters.

Button-Like Selected Row

  • The selected option now renders as a solid button row with a full background strip using SELECTION_BG.
  • Added an ◄ EXECUTE indicator on the right side of the selected row, giving clear feedback about which action will fire on Enter.

Keyboard Shortcut Emphasis

  • Maintained BOLD modifier on shortcut hints for both selected and unselected rows.

Before vs After

Before:

  [1 / y] Approve once
  [2 / a] Approve always for this kind
  [3 / d / n] Deny this call
  [Esc] Abort the turn

After:

  [1 / y] Approve once
  [2 / a] Approve always for this kind
  ──────────────────────────────────────────
  [3 / d / n] Deny this call
  [Esc] Abort the turn

Selected row now shows:

  [1 / y] Approve once              ◄ EXECUTE

Impact

  • Users can instantly identify the approve vs deny groups
  • The selected action is unmistakable with the background strip and EXECUTE indicator
  • Reduces accidental approvals/denials from misreading the option list

Generated with Claude Code

The sidebar was only showing when terminal width >= 100 columns, which is too restrictive for many terminal setups. Reduced the minimum width to 60 columns to make the sidebar visible in more common terminal configurations.

This fixes the issue where the sidebar would not appear in v0.8.62+ when using typical terminal sizes that are narrower than 100 columns.
Nightly builds:
- Add artifact existence check to skip redundant builds for the same commit
- Add build retry logic (up to 3 attempts) for transient failures
- Add nightly-complete summary job for branch protection rules
- Improve concurrency group to use ref_name instead of full ref

Auto-tag idempotency:
- Add semver validation for workspace version
- Add annotated tags with release metadata
- Add push retry logic with exponential backoff
- Fail fast if version consistency check fails
- Add concurrency control to prevent race conditions

Addresses v0.8.64 reliability concerns for nightly builds and auto-tagging.
Update SECURITY.md email address from the legacy deepseek-tui.com domain
to codewhale.com to match the project rebranding.

Addresses v0.8.64 security hardening requirements.
- Add visual separator between approve (0-1) and deny/abort (2-3) groups
- Render selected option as a solid button row with background strip
- Add 'EXECUTE' indicator on the selected row for clear action feedback
- Maintain keyboard shortcut emphasis with BOLD modifier

Improves UX by making the decision surface read as two distinct choice
clusters rather than a flat list, and gives the selected option a clear
button-like appearance.
@donglovejava donglovejava requested a review from Hmbown as a code owner June 23, 2026 03:26

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several updates, including adding new native tools to the tool catalog, reducing the minimum sidebar width, and enhancing the 'ApprovalWidget' with visual separators and selection highlights. It also includes several documentation files and a helper script ('fix_engine.py'). The code review feedback suggests improving the 'ApprovalWidget' layout by dynamically calculating the separator line width to prevent wrapping, padding the selected option background to span the full card width, and removing the unused 'locale_separator' helper. Additionally, it recommends replacing the hardcoded absolute Windows path in 'fix_engine.py' with a relative path for portability.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1185 to +1197
let separator = locale_separator(locale);

for (i, opt) in options.iter().enumerate() {
// Insert a visual separator between the approve group (0-1)
// and the deny/abort group (2-3) so the decision surface
// reads as two distinct choice clusters rather than a flat
// list.
if i == 2 {
lines.push(Line::from(vec![Span::styled(
separator,
Style::default().fg(palette::TEXT_MUTED),
)]));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The separator line is currently hardcoded to 40 characters. However, the approval card width is dynamic and can be as small as APPROVAL_CARD_MIN_WIDTH (40), which results in a content width of 36. In such cases, the 40-character separator line will wrap to the next line, breaking the visual layout.

We can dynamically construct the separator line using card_area.width to ensure it perfectly spans the available content width without wrapping.

Suggested change
let separator = locale_separator(locale);
for (i, opt) in options.iter().enumerate() {
// Insert a visual separator between the approve group (0-1)
// and the deny/abort group (2-3) so the decision surface
// reads as two distinct choice clusters rather than a flat
// list.
if i == 2 {
lines.push(Line::from(vec![Span::styled(
separator,
Style::default().fg(palette::TEXT_MUTED),
)]));
}
let content_width = card_area.width.saturating_sub(4) as usize;
for (i, opt) in options.iter().enumerate() {
// Insert a visual separator between the approve group (0-1)
// and the deny/abort group (2-3) so the decision surface
// reads as two distinct choice clusters rather than a flat
// list.
if i == 2 {
let sep_chars = "─".repeat(content_width.saturating_sub(2));
lines.push(Line::from(vec![Span::styled(
format!(" {sep_chars}"),
Style::default().fg(palette::TEXT_MUTED),
)]));
}

Comment on lines +1209 to +1231
if is_selected {
// Selected option: render as a solid button row with a
// distinct background strip so the user can instantly see
// which action will fire on Enter.
lines.push(Line::from(vec![
Span::styled(" ", Style::default().bg(palette::SELECTION_BG)),
Span::styled(
format!("[{}] ", opt.key_hint),
shortcut_style.add_modifier(Modifier::BOLD).bg(palette::SELECTION_BG),
),
Span::styled(
opt.label.to_string(),
option_style.add_modifier(Modifier::BOLD).bg(palette::SELECTION_BG),
),
Span::styled(" ", Style::default().bg(palette::SELECTION_BG)),
Span::styled(
selected_indicator(locale),
Style::default()
.fg(palette::SELECTION_TEXT)
.bg(palette::SELECTION_BG)
.add_modifier(Modifier::BOLD),
),
]));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The selected option background is currently applied only to the individual text spans. This means the background highlight will only cover the text itself, rather than forming a solid, full-width button row as described in the PR. Additionally, the ◄ EXECUTE indicator is placed immediately after the label instead of being right-aligned to the edge of the card.

We can resolve both issues by calculating the remaining width of the row and inserting a padding span of spaces with the SELECTION_BG background between the label and the indicator. This right-aligns the indicator and extends the background highlight across the entire width of the card.

            if is_selected {
                // Selected option: render as a solid button row with a
                // distinct background strip so the user can instantly see
                // which action will fire on Enter.
                let shortcut_text = format!("[{}] ", opt.key_hint);
                let label_text = opt.label.to_string();
                let indicator_text = selected_indicator(locale);
                let text_width = 2 + shortcut_text.width() + label_text.width() + indicator_text.width();
                let padding_width = content_width.saturating_sub(text_width);

                lines.push(Line::from(vec![
                    Span::styled("  ", Style::default().bg(palette::SELECTION_BG)),
                    Span::styled(
                        shortcut_text,
                        shortcut_style.add_modifier(Modifier::BOLD).bg(palette::SELECTION_BG),
                    ),
                    Span::styled(
                        label_text,
                        option_style.add_modifier(Modifier::BOLD).bg(palette::SELECTION_BG),
                    ),
                    Span::styled(
                        " ".repeat(padding_width),
                        Style::default().bg(palette::SELECTION_BG),
                    ),
                    Span::styled(
                        indicator_text,
                        Style::default()
                            .fg(palette::SELECTION_TEXT)
                            .bg(palette::SELECTION_BG)
                            .add_modifier(Modifier::BOLD),
                    ),
                ]));
            } else {

Comment on lines +1522 to +1527
fn locale_separator(locale: Locale) -> &'static str {
match locale {
Locale::ZhHans => " ────────────────────────────────────────",
_ => " ────────────────────────────────────────",
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the separator line is now constructed dynamically based on the card width, the locale_separator helper function is no longer used and can be safely removed.

Comment thread fix_engine.py
@@ -0,0 +1,39 @@
import re

file_path = r'C:\project\F_project1\CodeWhale\crates\tui\src\core\engine.rs'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The script uses a hardcoded absolute Windows path (C:\project\F_project1\...). This will fail on other developers' machines or in CI environments.

Using a relative path makes the script portable across different environments and operating systems.

Suggested change
file_path = r'C:\project\F_project1\CodeWhale\crates\tui\src\core\engine.rs'
file_path = 'crates/tui/src/core/engine.rs'

@Hmbown

Hmbown commented Jun 23, 2026

Copy link
Copy Markdown
Owner

I love this!! I've been fighting getting this down so your assistance is so appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants