Skip to content

Commit 9ba5d38

Browse files
committed
fix(tui): align with CC screenshots β€” spinner, bottom bar, markers
Based on actual CC screenshots: 1. Spinner char: braille β†’ `*` (CC uses asterisk, not braille) 2. Bottom bar: shows permission mode + "shift+tab to cycle" Processing state adds "Β· esc to interrupt" 3. Assistant response prefixed with `●` marker (CC style) 4. Remove welcome message (CC shows empty on fresh start) 5. Remove status bar permission mode (moved to bottom bar)
1 parent 7c56abd commit 9ba5d38

2 files changed

Lines changed: 55 additions & 11 deletions

File tree

β€Žcrates/tui/src/app.rsβ€Ž

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,7 @@ impl App {
175175
state: AppState::Idle,
176176
input: InputBox::new(),
177177
spinner: Spinner::new(),
178-
content_buffer: "Welcome! Type a message to start, or /help for commands.\n\n"
179-
.to_string(),
178+
content_buffer: String::new(),
180179
model_name: model_name.into(),
181180
permission_dialog: None,
182181
should_quit: false,
@@ -584,6 +583,10 @@ impl App {
584583
use crab_core::event::Event;
585584
match event {
586585
Event::ContentDelta { delta, .. } => {
586+
// Add ● marker at the start of assistant response (CC style)
587+
if self.state == AppState::Processing && !self.content_buffer.ends_with("● ") {
588+
self.content_buffer.push_str("● ");
589+
}
587590
// Track unseen content when the user is scrolled up
588591
if self.scroll_anchor.is_some() {
589592
// Count newlines in the delta as new "messages"
@@ -796,7 +799,13 @@ impl App {
796799
}
797800

798801
// Bottom bar
799-
render_bottom_bar(self.state, self.search.is_active(), layout.bottom_bar, buf);
802+
render_bottom_bar(
803+
self.state,
804+
self.search.is_active(),
805+
self.permission_mode,
806+
layout.bottom_bar,
807+
buf,
808+
);
800809

801810
// Autocomplete popup (renders above input)
802811
if self.autocomplete.is_active() {
@@ -1308,7 +1317,13 @@ fn format_token_count(tokens: u64) -> String {
13081317
}
13091318
}
13101319

1311-
fn render_bottom_bar(state: AppState, search_active: bool, area: Rect, buf: &mut Buffer) {
1320+
fn render_bottom_bar(
1321+
state: AppState,
1322+
search_active: bool,
1323+
perm_mode: crab_core::permission::PermissionMode,
1324+
area: Rect,
1325+
buf: &mut Buffer,
1326+
) {
13121327
let line = if search_active {
13131328
Line::from(Span::styled(
13141329
"Enter: next match | Esc: close | type to search",
@@ -1320,10 +1335,35 @@ fn render_bottom_bar(state: AppState, search_active: bool, area: Rect, buf: &mut
13201335
"y: allow | n: deny | a: always | Esc: deny",
13211336
Style::default().fg(Color::DarkGray),
13221337
)),
1323-
_ => Line::from(Span::styled(
1324-
"? for shortcuts",
1325-
Style::default().fg(Color::DarkGray),
1326-
)),
1338+
AppState::Processing => {
1339+
// CC shows: "β–Άβ–Ά accept edits on (shift+tab to cycle) Β· esc to interrupt"
1340+
Line::from(vec![
1341+
Span::styled(" β–Άβ–Ά ", Style::default().fg(Color::DarkGray)),
1342+
Span::styled(perm_mode.to_string(), Style::default().fg(Color::DarkGray)),
1343+
Span::styled(
1344+
" (shift+tab to cycle) Β· esc to interrupt",
1345+
Style::default().fg(Color::DarkGray),
1346+
),
1347+
])
1348+
}
1349+
_ => {
1350+
// CC shows: "β–Άβ–Ά accept edits on (shift+tab to cycle)" or "? for shortcuts"
1351+
if perm_mode == crab_core::permission::PermissionMode::Default {
1352+
Line::from(Span::styled(
1353+
" ? for shortcuts",
1354+
Style::default().fg(Color::DarkGray),
1355+
))
1356+
} else {
1357+
Line::from(vec![
1358+
Span::styled(" β–Άβ–Ά ", Style::default().fg(Color::DarkGray)),
1359+
Span::styled(perm_mode.to_string(), Style::default().fg(Color::DarkGray)),
1360+
Span::styled(
1361+
" (shift+tab to cycle)",
1362+
Style::default().fg(Color::DarkGray),
1363+
),
1364+
])
1365+
}
1366+
}
13271367
}
13281368
};
13291369
Widget::render(line, area, buf);
@@ -1348,7 +1388,7 @@ mod tests {
13481388
assert_eq!(app.state, AppState::Idle);
13491389
assert!(app.input.is_empty());
13501390
assert!(!app.spinner.is_active());
1351-
assert!(app.content_buffer.contains("Welcome"));
1391+
assert!(app.content_buffer.is_empty());
13521392
assert_eq!(app.model_name, "gpt-4o");
13531393
assert!(!app.should_quit);
13541394
assert!(!app.sidebar_visible);

β€Žcrates/tui/src/components/spinner.rsβ€Ž

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ use ratatui::widgets::Widget;
1313
// ── Spinner animation frames ──────────────────────────────────────────
1414

1515
/// Braille-based spinner frames for smooth animation.
16-
const FRAMES: &[&str] = &["β ‹", "β ™", "β Ή", "β Έ", "β Ό", "β ΄", "β ¦", "β §", "β ‡", "⠏"];
16+
/// CC uses `*` as spinner character (not braille). Matches the screenshot.
17+
const FRAMES: &[&str] = &["*"];
1718

1819
// ── CCB SPINNER_VERBS (188 verbs from constants/spinnerVerbs.ts) ──────
1920

@@ -525,7 +526,10 @@ mod tests {
525526
spinner.start_with_random_verb();
526527
assert_eq!(spinner.frame, 0);
527528
spinner.tick();
528-
assert_eq!(spinner.frame, 1);
529+
// With single-frame spinner (*), frame wraps back to 0
530+
assert_eq!(spinner.frame, 0);
531+
// But tick counter still advances
532+
assert_eq!(spinner.tick, 1);
529533
}
530534

531535
#[test]

0 commit comments

Comments
Β (0)