Skip to content

feat: 5 UX improvements — bell, help, spinner, quit confirm, Ctrl+R#46

Merged
kienbui1995 merged 1 commit intomainfrom
feat/ux-polish
Apr 13, 2026
Merged

feat: 5 UX improvements — bell, help, spinner, quit confirm, Ctrl+R#46
kienbui1995 merged 1 commit intomainfrom
feat/ux-polish

Conversation

@kienbui1995
Copy link
Copy Markdown
Owner

@kienbui1995 kienbui1995 commented Apr 12, 2026

UX Polish

# Fix Before After
1 Bell notification Silent when done \a bell alerts user in other tabs
2 /help readability 1 long line, 90+ commands Categorized: Navigation, Session, Agent, Code, Tools, Workflow, Config
3 Spinner animation Static ⟳ streaming... Animated ⠋ streaming... with rotating braille chars
4 Quit confirmation Instant exit Warns unsaved session, double-tap to exit
5 Ctrl+R history search Only Up/Down arrows Type query → Ctrl+R finds matching history entry

Note: Mouse scroll was already implemented ✅

197 tests, 0 warnings.

Summary by CodeRabbit

  • New Features

    • Added history search functionality—quickly find previous commands.
    • Reorganized help command with categorized command listings for easier navigation.
    • Added confirmation prompts when quitting with unsaved work or running tasks to prevent accidental data loss.
  • UI Improvements

    • Enhanced spinner animation for better visual feedback during operations.

1. Bell notification (\a) when agent finishes — alerts in other tabs
2. /help now categorized (Navigation, Session, Agent, Code, Tools, Workflow, Config)
3. Animated spinner (⠋⠙⠹...) during streaming and tool execution
4. /quit confirmation — warns about unsaved session, double-tap to exit
5. Ctrl+R reverse history search — type query then Ctrl+R to find match

197 tests, 0 warnings.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

The changes introduce three new interactive features: Ctrl+R history search, animated spinner during streaming/tool execution, and double-tap quit confirmation with unsaved session detection. Additionally, the help command output was restructured into categorized sections, and a terminal bell notification was added for stream completion events.

Changes

Cohort / File(s) Summary
History Search
mc/crates/mc-cli/src/main.rs, mc/crates/mc-tui/src/history.rs
Added Ctrl+R key binding to search input history and InputHistory::search() method for case-insensitive substring matching from newest to oldest entries.
Spinner Animation
mc/crates/mc-tui/src/app.rs, mc/crates/mc-tui/src/ui.rs
Added spinner_tick field and spinner_char() method to App for frame-based spinner animation; updated draw_status to use animated spinner glyph instead of static hardcoded character during streaming and tool execution.
Quit Confirmation & Help
mc/crates/mc-tui/src/app.rs, mc/crates/mc-tui/src/commands.rs
Added pending_quit field to App for double-tap quit confirmation; modified /quit command to check agent state and unsaved session state before exiting; restructured /help output into categorized sections (Navigation, Session, Agent, Code, Tools, Workflow, Config).
Stream Completion Feedback
mc/crates/mc-tui/src/app.rs
Added terminal bell notification (\x07) on StreamDone event after state reset and output trimming.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit hops through history with Ctrl+R in hand,
Spinner twirls with graceful turns across the land,
Quit confirms with care before goodbye,
Each feature makes the CLI fly,
Improvements spinning in the night so grand! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes all five main UX improvements introduced in the changeset: bell notification, help categorization, spinner animation, quit confirmation, and Ctrl+R history search.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ux-polish

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mc/crates/mc-tui/src/app.rs (1)

295-304: ⚠️ Potential issue | 🟡 Minor

This adds a second bell on every normal completion.

run_tui() already rings once after UiMessage::Done, so Line 303 makes successful turns beep twice. Keep the bell in one place, or split normal-complete vs. cancel paths explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mc/crates/mc-tui/src/app.rs` around lines 295 - 304, The AppEvent::StreamDone
handler in App (match arm for AppEvent::StreamDone) is emitting a bell via
eprint!("\x07") causing double beeps because run_tui() already rings on
UiMessage::Done; remove the unconditional eprint from the AppEvent::StreamDone
arm (or change it to only ring on cancellation paths) so only one bell is
produced—locate the AppEvent::StreamDone match in mc-tui's App (app.rs) and
delete or conditionally gate the eprint call to avoid duplicating the bell
triggered by run_tui()/UiMessage::Done.
🧹 Nitpick comments (2)
mc/crates/mc-tui/src/app.rs (1)

506-511: Add #[must_use] to spinner_char.

This is a new public value-returning method, and the crate convention is to mark those as #[must_use]. Based on learnings: Add #[must_use] attribute on all public functions returning values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mc/crates/mc-tui/src/app.rs` around lines 506 - 511, Add the #[must_use]
attribute to the public method spinner_char to follow crate convention for
public value-returning functions; locate the pub fn spinner_char(&mut self) ->
char in the file (it advances self.spinner_tick and returns from FRAMES) and
prepend #[must_use] so callers are warned if the returned char is ignored.
mc/crates/mc-tui/src/history.rs (1)

96-104: Add #[must_use] to search.

This is a new public value-returning API, and the crate convention is to mark those as #[must_use]. Based on learnings: Add #[must_use] attribute on all public functions returning values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mc/crates/mc-tui/src/history.rs` around lines 96 - 104, Add the #[must_use]
attribute to the public method search so its returned Option<&str> is annotated;
locate the pub fn search(&self, query: &str) -> Option<&str> declaration and
place #[must_use] immediately above it (follow crate convention to add
#[must_use] to public functions that return values).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mc/crates/mc-cli/src/main.rs`:
- Around line 631-643: The Ctrl+R handler currently reuses app.input as the
search query so after the first match the query becomes the full matched command
and subsequent Ctrl+R calls just re-find the same entry; modify the KeyEvent
branch handling KeyCode::Char('r') to capture the original query text (let
original_query = app.input.as_str().to_string()) and use a search cursor stored
on the app state (e.g., add a field like history_search_index or
history_search_matches on the App struct) to step through matches instead of
calling app.history.search(&app.input) repeatedly; implement logic so the first
Ctrl+R triggers a search (populate matches via
app.history.search_all(original_query) or similar), set cursor = 0 and set
app.input to matches[cursor], and subsequent Ctrl+R increments the cursor to
show the previous match (wrapping or stopping as desired), and reset the
cursor/matches when the user edits app.input.

In `@mc/crates/mc-tui/src/commands.rs`:
- Around line 19-34: Replace the hard-coded help text in the "/help" match arm
with generated output derived from the single source of truth
App::SLASH_COMMANDS: iterate App::SLASH_COMMANDS to build sections
(Navigation/Session/Agent/Code/Tools/Workflow/Config) and the tab-completion
list so both UI and completion always reflect the registry; ensure
App::SLASH_COMMANDS itself contains the missing entries (/branch, /summary,
/providers, /time, /rewind, /connect) and remove or stop referencing commands
that are not present in the registry (/plugin, /auto-test, /auto-commit,
/dry-run, /diff-preview), and update the completion handler to read from
App::SLASH_COMMANDS rather than a separate hardcoded list.
- Around line 35-44: App::submit_input currently drops all submissions when
app.state != AgentState::Idle which prevents the "/quit" double-tap path; change
submit logic to allow slash-commands that are safe (at least "/quit") to bypass
the non-idle gate by checking the raw input string and permitting "/quit" to be
processed even when state != Idle. In the "/quit" command handler (the match arm
handling "/quit"), flip the condition order so the pending_quit check is
evaluated first and can set should_quit = true on the second tap (e.g., if
app.pending_quit { app.should_quit = true } else { set pending_quit and push
warning }), and ensure that any non-"/quit" interaction clears app.pending_quit
(clear pending_quit in App::submit_input for normal inputs and in other command
handlers) so the pending_quit state doesn't persist incorrectly.

---

Outside diff comments:
In `@mc/crates/mc-tui/src/app.rs`:
- Around line 295-304: The AppEvent::StreamDone handler in App (match arm for
AppEvent::StreamDone) is emitting a bell via eprint!("\x07") causing double
beeps because run_tui() already rings on UiMessage::Done; remove the
unconditional eprint from the AppEvent::StreamDone arm (or change it to only
ring on cancellation paths) so only one bell is produced—locate the
AppEvent::StreamDone match in mc-tui's App (app.rs) and delete or conditionally
gate the eprint call to avoid duplicating the bell triggered by
run_tui()/UiMessage::Done.

---

Nitpick comments:
In `@mc/crates/mc-tui/src/app.rs`:
- Around line 506-511: Add the #[must_use] attribute to the public method
spinner_char to follow crate convention for public value-returning functions;
locate the pub fn spinner_char(&mut self) -> char in the file (it advances
self.spinner_tick and returns from FRAMES) and prepend #[must_use] so callers
are warned if the returned char is ignored.

In `@mc/crates/mc-tui/src/history.rs`:
- Around line 96-104: Add the #[must_use] attribute to the public method search
so its returned Option<&str> is annotated; locate the pub fn search(&self,
query: &str) -> Option<&str> declaration and place #[must_use] immediately above
it (follow crate convention to add #[must_use] to public functions that return
values).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 07219dac-f2d5-452c-b9e6-cbd22611b768

📥 Commits

Reviewing files that changed from the base of the PR and between a9c2f74 and b65288f.

📒 Files selected for processing (5)
  • mc/crates/mc-cli/src/main.rs
  • mc/crates/mc-tui/src/app.rs
  • mc/crates/mc-tui/src/commands.rs
  • mc/crates/mc-tui/src/history.rs
  • mc/crates/mc-tui/src/ui.rs

Comment on lines +631 to +643
event::KeyEvent {
code: KeyCode::Char('r'),
modifiers,
..
} if modifiers.contains(KeyModifiers::CONTROL) => {
// Ctrl+R: reverse history search
let query = app.input.as_str().to_string();
if !query.is_empty() {
if let Some(found) = app.history.search(&query) {
app.input.set(found);
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Ctrl+R can't walk past the first match.

Line 637 reuses the current input buffer as the next query, so after the first hit the query becomes the full matched command and the next Ctrl+R re-finds the same history entry. Preserve the original search text plus a match index/cursor if you want repeated Ctrl+R to step backward through matches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mc/crates/mc-cli/src/main.rs` around lines 631 - 643, The Ctrl+R handler
currently reuses app.input as the search query so after the first match the
query becomes the full matched command and subsequent Ctrl+R calls just re-find
the same entry; modify the KeyEvent branch handling KeyCode::Char('r') to
capture the original query text (let original_query =
app.input.as_str().to_string()) and use a search cursor stored on the app state
(e.g., add a field like history_search_index or history_search_matches on the
App struct) to step through matches instead of calling
app.history.search(&app.input) repeatedly; implement logic so the first Ctrl+R
triggers a search (populate matches via app.history.search_all(original_query)
or similar), set cursor = 0 and set app.input to matches[cursor], and subsequent
Ctrl+R increments the cursor to show the previous match (wrapping or stopping as
desired), and reset the cursor/matches when the user edits app.input.

Comment on lines +19 to +34
"/help" => {
app.push("━━━ Navigation ━━━");
app.push(" /help /quit /clear /status /version /whoami /theme /vim");
app.push("━━━ Session ━━━");
app.push(" /save /load /sessions /fork /branches /switch /history /search /export /resume");
app.push("━━━ Agent ━━━");
app.push(" /model /models /plan /thinking /effort /compact /tokens /context /cost /retry");
app.push("━━━ Code ━━━");
app.push(" /diff /log /commit /stash /undo /review /security-review /debug /template /spec");
app.push("━━━ Tools ━━━");
app.push(" /run /grep /tree /cat /head /tail /files /wc /pwd /env /open /size /image");
app.push("━━━ Workflow ━━━");
app.push(" /todo /test /ship /auto-test /auto-commit /dry-run /diff-preview /loop /cron");
app.push("━━━ Config ━━━");
app.push(" /config /permissions /init /add /memory /pin /alias /copy /agents /plugin");
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

/help, completion, and the command handler are already drifting.

This block omits implemented commands like /branch, /summary, /providers, /time, /rewind, and /connect, while entries exposed here such as /plugin, /auto-test, /auto-commit, /dry-run, and /diff-preview still are not in App::SLASH_COMMANDS. Please generate help text and tab-completion from one command registry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mc/crates/mc-tui/src/commands.rs` around lines 19 - 34, Replace the
hard-coded help text in the "/help" match arm with generated output derived from
the single source of truth App::SLASH_COMMANDS: iterate App::SLASH_COMMANDS to
build sections (Navigation/Session/Agent/Code/Tools/Workflow/Config) and the
tab-completion list so both UI and completion always reflect the registry;
ensure App::SLASH_COMMANDS itself contains the missing entries (/branch,
/summary, /providers, /time, /rewind, /connect) and remove or stop referencing
commands that are not present in the registry (/plugin, /auto-test,
/auto-commit, /dry-run, /diff-preview), and update the completion handler to
read from App::SLASH_COMMANDS rather than a separate hardcoded list.

Comment on lines +35 to +44
"/quit" => {
if app.state != AgentState::Idle {
app.push("⚠ Agent is running. Press /quit again to force exit.");
app.pending_quit = true;
} else if !app.pending_quit && app.total_input_tokens > 0 {
app.push("⚠ Unsaved session. /quit again to exit, or /save first.");
app.pending_quit = true;
} else {
app.should_quit = true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The double-tap /quit flow isn't wired end-to-end yet.

App::submit_input() still drops all submits while app.state != AgentState::Idle, so this busy-agent branch cannot be reached from the input box. Even after relaxing that, Line 36 still wins on the second /quit, so the promised force-exit path never reaches should_quit = true, and pending_quit is never cleared after other interactions.

🛠 Suggested direction
         "/quit" => {
-            if app.state != AgentState::Idle {
+            if app.pending_quit {
+                app.should_quit = true;
+            } else if app.state != AgentState::Idle {
                 app.push("⚠ Agent is running. Press /quit again to force exit.");
                 app.pending_quit = true;
-            } else if !app.pending_quit && app.total_input_tokens > 0 {
+            } else if app.total_input_tokens > 0 {
                 app.push("⚠ Unsaved session. /quit again to exit, or /save first.");
                 app.pending_quit = true;
             } else {
                 app.should_quit = true;
             }
         },

You'll also need to clear pending_quit on any non-/quit interaction, and let safe slash commands like /quit bypass the non-idle submit gate.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"/quit" => {
if app.state != AgentState::Idle {
app.push("⚠ Agent is running. Press /quit again to force exit.");
app.pending_quit = true;
} else if !app.pending_quit && app.total_input_tokens > 0 {
app.push("⚠ Unsaved session. /quit again to exit, or /save first.");
app.pending_quit = true;
} else {
app.should_quit = true;
}
"/quit" => {
if app.pending_quit {
app.should_quit = true;
} else if app.state != AgentState::Idle {
app.push("⚠ Agent is running. Press /quit again to force exit.");
app.pending_quit = true;
} else if app.total_input_tokens > 0 {
app.push("⚠ Unsaved session. /quit again to exit, or /save first.");
app.pending_quit = true;
} else {
app.should_quit = true;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mc/crates/mc-tui/src/commands.rs` around lines 35 - 44, App::submit_input
currently drops all submissions when app.state != AgentState::Idle which
prevents the "/quit" double-tap path; change submit logic to allow
slash-commands that are safe (at least "/quit") to bypass the non-idle gate by
checking the raw input string and permitting "/quit" to be processed even when
state != Idle. In the "/quit" command handler (the match arm handling "/quit"),
flip the condition order so the pending_quit check is evaluated first and can
set should_quit = true on the second tap (e.g., if app.pending_quit {
app.should_quit = true } else { set pending_quit and push warning }), and ensure
that any non-"/quit" interaction clears app.pending_quit (clear pending_quit in
App::submit_input for normal inputs and in other command handlers) so the
pending_quit state doesn't persist incorrectly.

@kienbui1995 kienbui1995 merged commit 61a755e into main Apr 13, 2026
9 checks passed
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.

1 participant