Skip to content

Commit 8de0f32

Browse files
cursoragentechobt
andcommitted
fix(tui): render WelcomeCard mascot in the docs demo
Stop drawing a parallel welcome card in the GIF recorder. Empty-session frames now paint the same WelcomeCard and MASCOT_MINIMAL_LINES the TUI uses, with snapshot coverage on the four block-brain glyphs. Co-authored-by: Mathis <echobt@users.noreply.github.com>
1 parent 226ec1a commit 8de0f32

8 files changed

Lines changed: 199 additions & 75 deletions

File tree

docs/guides/tui.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ The timeline is the transcript. It renders several kinds of row:
4141
| Tool call | ``/`` then the tool name and a short argument summary | A tool the agent invoked |
4242
| Tool result | `⎿ …` indented under the call | What the tool returned |
4343
| Subagent task | `● Task <type>` with a todo list underneath | Work delegated to a subagent |
44-
| Welcome card | A bordered card | Shown while the session is empty |
44+
| Welcome card | Bordered card with the ASCII mascot, greeting, and tips | Shown while the session is empty |
4545

4646
Tool rows collapse to a summary. Press `e` while the timeline has focus to
4747
expand or collapse the details of the selected tool call.

src/cortex-tui-capture/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ cortex-tui-core = { workspace = true }
3030
# Product theme, so the recorded demo tracks the real TUI palette
3131
cortex-core = { workspace = true }
3232

33+
# Same welcome card / mascot the live TUI mounts, so the GIF cannot drift
34+
cortex-tui-components = { workspace = true }
35+
3336
# Async runtime
3437
tokio = { workspace = true, features = ["full", "sync", "time", "macros", "fs"] }
3538

src/cortex-tui-capture/src/demo/mod.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ mod tests {
241241
let recording = recording();
242242
let first = &recording.frames[0];
243243
assert!(
244-
first.plain.contains("Cortex Code"),
244+
first.plain.contains("Cortex CLI"),
245245
"welcome frame missing the product name:\n{}",
246246
first.plain
247247
);
@@ -250,6 +250,36 @@ mod tests {
250250
"welcome frame missing the public endpoint:\n{}",
251251
first.plain
252252
);
253+
for glyph in cortex_tui_components::mascot::MASCOT_MINIMAL_LINES {
254+
let needle = glyph.trim();
255+
assert!(
256+
first.plain.contains(needle),
257+
"welcome frame missing mascot line {needle:?}:\n{}",
258+
first.plain
259+
);
260+
}
261+
assert!(
262+
first.plain.contains("Welcome!"),
263+
"welcome frame missing the greeting:\n{}",
264+
first.plain
265+
);
266+
}
267+
268+
#[test]
269+
fn welcome_beats_keep_the_mascot_on_screen() {
270+
let recording = recording();
271+
for frame in recording
272+
.frames
273+
.iter()
274+
.filter(|frame| frame.label == "welcome")
275+
{
276+
assert!(
277+
frame.plain.contains("▄█▀▀▀▀█▄"),
278+
"welcome beat {} lost the mascot:\n{}",
279+
frame.index,
280+
frame.plain
281+
);
282+
}
253283
}
254284

255285
#[test]

src/cortex-tui-capture/src/demo/render.rs

Lines changed: 40 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
//! real TUI palette instead of a second, drifting copy of it.
55
66
use cortex_core::style::{
7-
BORDER, BORDER_FOCUS, CYAN_PRIMARY, SKY_BLUE, SUCCESS, SURFACE_0, TEXT, TEXT_DIM, TEXT_MUTED,
8-
VOID,
7+
BORDER, BORDER_FOCUS, CYAN_PRIMARY, SKY_BLUE, SUCCESS, TEXT, TEXT_DIM, TEXT_MUTED, VOID,
98
};
9+
use cortex_tui_components::welcome_card::{InfoCard, InfoCardPair, ToLines, WelcomeCard};
1010
use ratatui::Frame;
1111
use ratatui::layout::{Constraint, Layout, Rect};
1212
use ratatui::style::{Modifier, Style};
@@ -47,56 +47,46 @@ pub fn draw_scene(frame: &mut Frame, scene: &Scene) {
4747
draw_hints(frame, hints_area, scene);
4848
}
4949

50+
/// Empty-session frame: the same `WelcomeCard` + info cards the live TUI paints
51+
/// via `generate_welcome_lines`, including `MASCOT_MINIMAL_LINES`.
5052
fn draw_welcome(frame: &mut Frame, area: Rect, scene: &Scene) {
51-
let card_width = area.width.min(82);
52-
let card_height = 11.min(area.height);
53-
let card = Rect {
54-
x: area.x,
55-
y: area.y + area.height.saturating_sub(card_height) / 2,
56-
width: card_width,
57-
height: card_height,
58-
};
59-
60-
let block = Block::default()
61-
.borders(Borders::ALL)
62-
.border_type(BorderType::Rounded)
63-
.border_style(Style::default().fg(BORDER))
64-
.style(Style::default().bg(SURFACE_0))
65-
.padding(Padding::new(2, 2, 1, 0))
66-
.title(Span::styled(
67-
" Cortex Code ",
68-
Style::default()
69-
.fg(CYAN_PRIMARY)
70-
.add_modifier(Modifier::BOLD),
71-
));
72-
73-
let lines = vec![
74-
Line::from(Span::styled(
75-
"A coding agent that reads, edits, runs and tests your project.",
76-
Style::default().fg(TEXT),
77-
)),
78-
Line::default(),
79-
field_line("workspace", &scene.workspace),
80-
field_line("endpoint", &scene.endpoint),
81-
field_line(
82-
"mode",
83-
&format!("{} · {} autonomy", scene.mode, scene.autonomy),
84-
),
85-
Line::default(),
86-
Line::from(Span::styled(
87-
"Describe a change and Cortex Code works through it, tool call by tool call.",
88-
Style::default().fg(TEXT_MUTED),
89-
)),
90-
];
91-
92-
frame.render_widget(Paragraph::new(lines).block(block), card);
93-
}
53+
let welcome_card = WelcomeCard::new()
54+
.subtitle("Your AI-powered coding assistant.")
55+
.version(env!("CARGO_PKG_VERSION"))
56+
.tips(&[
57+
"Send /help for available commands.",
58+
"Use Tab for autocomplete. Press Esc to cancel.",
59+
])
60+
.accent_color(CYAN_PRIMARY)
61+
.text_color(TEXT)
62+
.dim_color(TEXT_DIM)
63+
.border_color(CYAN_PRIMARY);
64+
65+
let mut lines = welcome_card.to_lines(area.width);
66+
lines.push(Line::default());
67+
68+
let left_card = InfoCard::new()
69+
.add("Directory", &scene.workspace)
70+
.add("Endpoint", &scene.endpoint)
71+
.dim_color(TEXT_DIM)
72+
.text_color(TEXT)
73+
.border_color(CYAN_PRIMARY);
74+
75+
let right_card = InfoCard::new()
76+
.add("Mode", &scene.mode)
77+
.add("Autonomy", &scene.autonomy)
78+
.dim_color(TEXT_DIM)
79+
.text_color(TEXT)
80+
.border_color(CYAN_PRIMARY);
81+
82+
lines.extend(
83+
InfoCardPair::new(left_card, right_card)
84+
.gap(2)
85+
.right_width(25)
86+
.to_lines(area.width),
87+
);
9488

95-
fn field_line(label: &str, value: &str) -> Line<'static> {
96-
Line::from(vec![
97-
Span::styled(format!("{label:<11}"), Style::default().fg(TEXT_MUTED)),
98-
Span::styled(value.to_string(), Style::default().fg(TEXT_DIM)),
99-
])
89+
frame.render_widget(Paragraph::new(lines).style(Style::default().bg(VOID)), area);
10090
}
10191

10292
fn draw_timeline(frame: &mut Frame, area: Rect, scene: &Scene) {

src/cortex-tui-components/src/mascot.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,16 @@ mod tests {
171171
let expr = MascotExpression::default();
172172
assert_eq!(expr.art(), MASCOT);
173173
}
174+
175+
#[test]
176+
fn minimal_mascot_is_four_block_brain_lines() {
177+
assert_eq!(MASCOT_MINIMAL_LINES.len(), 4);
178+
assert_eq!(MASCOT_MINIMAL_LINES[0].trim(), "▄█▀▀▀▀█▄");
179+
assert_eq!(MASCOT_MINIMAL_LINES[1].trim(), "██ ▌ ▐ ██");
180+
assert_eq!(MASCOT_MINIMAL_LINES[2].trim(), "█▄▄▄▄▄▄█");
181+
assert_eq!(MASCOT_MINIMAL_LINES[3].trim(), "█ █");
182+
assert!(MASCOT_MINIMAL.contains("▄█▀▀▀▀█▄"));
183+
assert!(MASCOT_MINIMAL.contains("▌"));
184+
assert!(MASCOT_MINIMAL.contains("▐"));
185+
}
174186
}

src/cortex-tui-components/src/welcome_card.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,4 +630,77 @@ mod tests {
630630
let card = WelcomeCard::new().tips(&["Tip 1", "Tip 2"]);
631631
assert!(card.required_height() >= 10);
632632
}
633+
634+
fn buffer_text(buf: &Buffer) -> String {
635+
let area = buf.area();
636+
let mut out = String::new();
637+
for y in 0..area.height {
638+
for x in 0..area.width {
639+
out.push_str(buf[(x, y)].symbol());
640+
}
641+
out.push('\n');
642+
}
643+
out
644+
}
645+
646+
fn assert_contains_mascot(text: &str) {
647+
for line in MASCOT_MINIMAL_LINES {
648+
let needle = line.trim();
649+
assert!(
650+
text.contains(needle),
651+
"missing mascot line {needle:?}:\n{text}"
652+
);
653+
}
654+
}
655+
656+
#[test]
657+
fn welcome_card_to_lines_includes_mascot() {
658+
let card = WelcomeCard::new()
659+
.subtitle("Your AI-powered coding assistant.")
660+
.tips(&[
661+
"Send /help for available commands.",
662+
"Use Tab for autocomplete. Press Esc to cancel.",
663+
]);
664+
let text = card
665+
.to_lines(80)
666+
.into_iter()
667+
.map(|line| line.to_string())
668+
.collect::<Vec<_>>()
669+
.join("\n");
670+
assert_contains_mascot(&text);
671+
assert!(text.contains("Welcome!"));
672+
assert!(text.contains("Cortex CLI"));
673+
}
674+
675+
#[test]
676+
fn welcome_card_widget_renders_mascot() {
677+
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 16));
678+
WelcomeCard::new()
679+
.subtitle("Your AI-powered coding assistant.")
680+
.tips(&["Send /help for available commands."])
681+
.render(Rect::new(0, 0, 80, 16), &mut buf);
682+
let text = buffer_text(&buf);
683+
assert_contains_mascot(&text);
684+
assert!(text.contains("Welcome!"));
685+
}
686+
687+
#[test]
688+
fn welcome_card_narrow_and_wide_viewports_keep_the_mascot() {
689+
for (width, height) in [(40, 12), (120, 40)] {
690+
let card = WelcomeCard::new().tips(&["Send /help for available commands."]);
691+
let text = card
692+
.to_lines(width)
693+
.into_iter()
694+
.map(|line| line.to_string())
695+
.collect::<Vec<_>>()
696+
.join("\n");
697+
assert_contains_mascot(&text);
698+
699+
let mut buf = Buffer::empty(Rect::new(0, 0, width, height));
700+
WelcomeCard::new()
701+
.tips(&["Send /help for available commands."])
702+
.render(Rect::new(0, 0, width, height), &mut buf);
703+
assert_contains_mascot(&buffer_text(&buf));
704+
}
705+
}
633706
}

src/cortex-tui/src/runner/login_screen.rs

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use ratatui::widgets::{Clear, Paragraph};
1919
use tokio::sync::mpsc;
2020

2121
use cortex_login::{SecureAuthData, save_auth_with_fallback};
22+
use cortex_tui_components::mascot::MASCOT_MINIMAL_LINES;
2223
use cortex_tui_components::spinner::SpinnerStyle;
2324

2425
// ============================================================================
@@ -398,27 +399,29 @@ impl LoginScreen {
398399
]));
399400
f.render_widget(welcome, chunks[0]);
400401

401-
// Line 3: Mascot top
402-
let mascot_top = Paragraph::new(" ▄█▀▀▀▀█▄ ").style(Style::default().fg(PRIMARY));
403-
f.render_widget(mascot_top, chunks[2]);
404-
405-
// Line 4: Mascot + waiting message
406-
let mascot_middle = Paragraph::new(Line::from(vec![
407-
Span::styled("██ ▌ ▐ ██ ", Style::default().fg(PRIMARY)),
408-
Span::styled(
409-
format!("Waiting for browser authentication {}", spinner),
410-
Style::default().fg(PRIMARY),
411-
),
412-
]));
413-
f.render_widget(mascot_middle, chunks[3]);
414-
415-
// Line 5: Mascot bottom
416-
let mascot_bottom = Paragraph::new(" █▄▄▄▄▄▄█ ").style(Style::default().fg(PRIMARY));
417-
f.render_widget(mascot_bottom, chunks[4]);
418-
419-
// Line 6: Mascot legs
420-
let mascot_legs = Paragraph::new(" █ █").style(Style::default().fg(PRIMARY));
421-
f.render_widget(mascot_legs, chunks[5]);
402+
// Lines 3-6: official welcome mascot (`MASCOT_MINIMAL_LINES`).
403+
f.render_widget(
404+
Paragraph::new(MASCOT_MINIMAL_LINES[0]).style(Style::default().fg(PRIMARY)),
405+
chunks[2],
406+
);
407+
f.render_widget(
408+
Paragraph::new(Line::from(vec![
409+
Span::styled(MASCOT_MINIMAL_LINES[1], Style::default().fg(PRIMARY)),
410+
Span::styled(
411+
format!(" Waiting for browser authentication {spinner}"),
412+
Style::default().fg(PRIMARY),
413+
),
414+
])),
415+
chunks[3],
416+
);
417+
f.render_widget(
418+
Paragraph::new(MASCOT_MINIMAL_LINES[2]).style(Style::default().fg(PRIMARY)),
419+
chunks[4],
420+
);
421+
f.render_widget(
422+
Paragraph::new(MASCOT_MINIMAL_LINES[3]).style(Style::default().fg(PRIMARY)),
423+
chunks[5],
424+
);
422425

423426
// Line 8: Browser message
424427
let copy_hint = if self.copied_notification.is_some() {
@@ -960,6 +963,13 @@ mod tests {
960963
|| waiting.contains("Cortex"),
961964
"{waiting}"
962965
);
966+
for glyph in MASCOT_MINIMAL_LINES {
967+
let needle = glyph.trim();
968+
assert!(
969+
waiting.contains(needle),
970+
"login waiting screen missing mascot line {needle:?}: {waiting}"
971+
);
972+
}
963973

964974
screen.state = LoginState::SelectMethod;
965975
screen.error_message = Some("The coding service is temporarily unavailable".into());

src/cortex-tui/src/views/minimal_session/tests.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,15 @@ mod harness_snapshots {
4545
dump_snapshot("home", &text);
4646
assert!(!text.to_lowercase().contains("grok"));
4747
assert!(
48-
text.contains("Cortex") || text.contains("session") || !text.trim().is_empty(),
49-
"home session should render: {text}"
48+
text.contains("Cortex CLI"),
49+
"home session should render WelcomeCard: {text}"
5050
);
51+
for glyph in ["▄█▀▀▀▀█▄", "██ ▌ ▐ ██", "█▄▄▄▄▄▄█", "█ █"] {
52+
assert!(
53+
text.contains(glyph),
54+
"home session missing mascot line {glyph:?}: {text}"
55+
);
56+
}
5157
}
5258

5359
#[test]

0 commit comments

Comments
 (0)