use crate::tui::{
state::{MissionControlState, WelcomeRenderCache},
theme::MissionControlTheme,
};
use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap},
};
const LOGO: [&str; 10] = [
"███╗ ███╗",
"████╗ ████║",
"██╔██╗ ██╔██║ ███╗ ███╗ █████╗ ██████╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗",
"██║╚██╗ ██╔╝██║ ████╗ ████║ ██╔══██╗ ██╔════╝ ██║ ██╔════╝ ██╔═══██╗ ██╔══██╗ ██╔════╝",
"██║ ╚████╔╝ ██║ ██╔████╔██║ ███████║ ██║ ███╗ ██║ ███╗██║ ██║ ██║ ██║ ██║ █████╗",
"██║ ╚██╔╝ ██║ ██║╚██╔╝██║ ██╔══██║ ██║ ██║ ██║ ╚══╝██║ ██║ ██║ ██║ ██║ ██╔══╝",
"██║ ╚═╝ ██║ ██║ ╚═╝ ██║ ██║ ██║ ╚██████╔╝ ██║ ╚██████╗ ╚██████╔╝ ██████╔╝ ███████╗",
"██║ ██║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
"██║ ██║",
"╚═╝ ╚═╝",
];
const LOGO_WIDTH: u16 = 96;
const FULL_WELCOME_MIN_HEIGHT: u16 = 33;
const SHORTCUTS: [(&str, &str); 10] = [
("[Alt-A]", "Show/hide right panel"),
("[Tab/Shift-Tab]", "Next / previous pane"),
("[Alt-P]", "Cycle primary agent"),
("[Alt-1/2]", "Activity / Summary tab"),
("/skills", "Manage global / project skills"),
("/models", "Manage available models"),
(
"/summarize-start | /summarize-stop",
"Start / stop Summary agent",
),
("/fast", "Toggle Fast mode where supported"),
("/compact", "Compact current session"),
("/update", "Update Magi-Code"),
];
pub(super) fn draw_welcome(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
if area.is_empty() {
return;
}
let theme = state.theme;
let width = area.width.saturating_sub(2).clamp(1, LOGO_WIDTH);
let key = (theme, width, area.height);
let logo_phase = state
.welcome_animation_active()
.then_some(state.welcome_animation_phase);
let mut cached = state.welcome_render_cache.borrow_mut();
if cached.as_ref().is_none_or(|cache| {
cache.key != key || cache.update_version != state.available_update_version
}) {
let (update_notice, update_height) = welcome_update_notice(width, state);
let welcome_height = area.height.saturating_sub(update_height);
let compact = width < LOGO_WIDTH || welcome_height < FULL_WELCOME_MIN_HEIGHT;
let mut lines = welcome_lines(width, compact, welcome_height, state);
let height = (lines.len() as u16).min(welcome_height);
let logo_height = if compact { 1 } else { LOGO.len() as u16 };
let logo_height = if update_notice.is_some() {
logo_height.min(welcome_height.saturating_sub(2))
} else {
logo_height
};
let body = lines.split_off(usize::from(logo_height));
*cached = Some(WelcomeRenderCache {
key,
update_version: state.available_update_version.clone(),
update_notice,
update_height,
compact,
logo_phase,
logo: Paragraph::new(lines).style(theme.pane()),
body: Paragraph::new(body).style(theme.pane()),
height,
logo_height,
});
}
let cache = cached.as_mut().expect("welcome layout cached");
if cache.logo_phase != logo_phase {
cache.logo = Paragraph::new(welcome_logo_lines(cache.compact, state)).style(theme.pane());
cache.logo_phase = logo_phase;
}
let height = cache.height.min(area.height);
let content = Rect::new(
area.x + area.width.saturating_sub(width) / 2,
area.y
+ area
.height
.saturating_sub(height.saturating_add(cache.update_height))
/ 2,
width,
height,
);
if state.welcome_animation_active() {
draw_rain(frame, area, content, theme, state.welcome_animation_phase);
}
frame.render_widget(Clear, content);
let logo_height = cache.logo_height.min(height);
frame.render_widget(
&cache.logo,
Rect::new(content.x, content.y, width, logo_height),
);
frame.render_widget(
&cache.body,
Rect::new(
content.x,
content.y + logo_height,
width,
height - logo_height,
),
);
if let Some(notice) = &cache.update_notice {
let notice_area = Rect::new(
content.x,
content.bottom().saturating_add(1).min(area.bottom()),
width,
cache.update_height.saturating_sub(1).min(
area.bottom()
.saturating_sub(content.bottom().saturating_add(1)),
),
);
frame.render_widget(Clear, notice_area);
frame.render_widget(notice, notice_area);
}
}
fn welcome_logo_lines(compact: bool, state: &MissionControlState) -> Vec<Line<'static>> {
if compact {
vec![logo_line("MAGI CODE", state).centered()]
} else {
LOGO.iter()
.map(|row| {
let padded = format!("{row:<width$}", width = usize::from(LOGO_WIDTH));
logo_line(&padded, state).centered()
})
.collect()
}
}
fn welcome_update_notice(
width: u16,
state: &MissionControlState,
) -> (Option<Paragraph<'static>>, u16) {
let theme = state.theme;
let update_lines = state.available_update_version.as_ref().map(|version| {
vec![
Line::styled(
format!("magi-code {version} is available."),
theme.muted(false),
),
Line::from(vec![
Span::styled(
"/update",
Style::default()
.fg(theme.text_command())
.add_modifier(Modifier::BOLD),
),
Span::styled(" to install and reopen this session.", theme.muted(false)),
]),
]
});
let update_height = update_lines.as_ref().map_or(0, |lines| {
let inner_width = usize::from(width.saturating_sub(2).max(1));
let rows: usize = lines
.iter()
.map(|line| line.width().div_ceil(inner_width))
.sum();
u16::try_from(rows).unwrap_or(u16::MAX).saturating_add(3)
});
let notice = update_lines.map(|lines| {
Paragraph::new(lines)
.style(theme.pane())
.centered()
.wrap(Wrap { trim: true })
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.text_accent())),
)
});
(notice, update_height)
}
fn welcome_lines(
width: u16,
compact: bool,
welcome_height: u16,
state: &MissionControlState,
) -> Vec<Line<'static>> {
let theme = state.theme;
let accent = Style::default().fg(theme.text_accent());
let command = Style::default()
.fg(theme.text_command())
.add_modifier(Modifier::BOLD);
let mut lines = welcome_logo_lines(compact, state);
if !compact {
lines.push(Line::default());
lines.push(Line::styled("Follow the white rabbit, but take your time.", accent).centered());
lines.push(Line::default());
for row in [r"(\_/)", r"( •_•)", r"/ >_ "] {
lines.push(Line::styled(row, theme.muted(false)).centered());
}
}
lines.push(Line::default());
for (index, (key, description)) in SHORTCUTS.into_iter().enumerate() {
if index == 0 || index == 4 {
if index == 4 {
lines.push(Line::default());
}
let heading = if index == 0 {
"Useful hotkeys"
} else {
"Useful Commands"
};
lines.push(Line::styled(heading, accent.add_modifier(Modifier::BOLD)));
}
let full = Line::from(vec![
Span::styled(key, command),
Span::styled(format!(" {description}"), theme.muted(false)),
]);
if full.width() <= usize::from(width) {
lines.push(full);
} else if key.len() <= usize::from(width) {
lines.push(Line::styled(key, command));
} else {
for part in key.split(" | ") {
lines.push(Line::styled(part, command));
}
}
}
if state.available_update_version.is_some() {
lines.truncate(usize::from(welcome_height.saturating_sub(2)));
}
lines.push(Line::default());
lines.push(Line::styled("Type a prompt below to begin.", theme.muted(false)).centered());
lines
}
fn logo_line(text: &str, state: &MissionControlState) -> Line<'static> {
let style = Style::default()
.fg(state.theme.text_accent())
.add_modifier(Modifier::BOLD);
if state.welcome_animation_active() {
Line::from(super::super::prompt::fast_mode_rainbow_spans(
text,
state.welcome_animation_phase,
style,
state.theme,
))
} else {
Line::styled(text.to_owned(), style)
}
}
fn draw_rain(
frame: &mut Frame<'_>,
area: Rect,
opaque_content: Rect,
theme: MissionControlTheme,
phase: usize,
) {
const GLYPHS: &[u8] = b"01.:|+*";
for column in (0..area.width).step_by(3) {
let seed = usize::from(column).wrapping_mul(2654435761);
let tail_length = 4 + seed % 7;
let cycle = usize::from(area.height) + tail_length + 9;
let head = (phase / (2 + seed % 3)).wrapping_add(seed) % cycle;
for tail in 0..tail_length {
let Some(row) = head
.checked_sub(tail)
.filter(|&row| row < usize::from(area.height))
else {
continue;
};
let position = (area.x + column, area.y + row as u16);
if opaque_content.contains(position.into()) {
continue;
}
let color = if tail == 0 {
theme.text_accent()
} else {
theme.text_muted()
};
let glyph = GLYPHS[seed.wrapping_add(row).wrapping_add(phase / 6) % GLYPHS.len()];
frame.buffer_mut()[position]
.set_char(char::from(glyph))
.set_style(Style::default().fg(color).add_modifier(Modifier::DIM));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::{
render::{test_support::find_text, transcript::draw_transcript},
state::MissionControlState,
};
use ratatui::{Terminal, backend::TestBackend};
fn draw_fresh_welcome(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
if area.is_empty() {
return;
}
let width = area.width.saturating_sub(2).clamp(1, LOGO_WIDTH);
let lines = welcome_lines(
width,
width < LOGO_WIDTH || area.height < FULL_WELCOME_MIN_HEIGHT,
area.height,
state,
);
let height = (lines.len() as u16).min(area.height);
let content = Rect::new(
area.x + area.width.saturating_sub(width) / 2,
area.y + area.height.saturating_sub(height) / 2,
width,
height,
);
if state.welcome_animation_active() {
draw_rain(
frame,
area,
Rect::default(),
state.theme,
state.welcome_animation_phase,
);
}
frame.render_widget(Clear, content);
frame.render_widget(Paragraph::new(lines).style(state.theme.pane()), content);
}
#[test]
fn welcome_cache_updates_notice_when_added_changed_removed_or_resized() {
let mut state = MissionControlState::default();
let mut terminal = Terminal::new(TestBackend::new(100, 40)).unwrap();
let mut fresh = Terminal::new(TestBackend::new(100, 40)).unwrap();
for (version, width, height) in [
(None, 100, 40),
(Some("99.0.0"), 100, 40),
(Some("99.1.0"), 100, 40),
(Some("99.1.0"), 80, 30),
(Some("99.1.0"), 40, 28),
(Some("99.1.0"), 40, 23),
(Some("99.1.0"), 1, 1),
(Some("99.1.0"), 100, 6),
(Some("99.1.0"), 100, 7),
(None, 100, 40),
(Some("99.2.0"), 100, 40),
] {
state.available_update_version = version.map(str::to_owned);
let area = Rect::new(0, 0, width, height);
for phase in [0, 1, 2] {
state.welcome_animation_phase = phase;
terminal
.draw(|frame| draw_welcome(frame, area, &state))
.unwrap();
let fresh_state = MissionControlState {
available_update_version: version.map(str::to_owned),
welcome_animation_phase: phase,
..Default::default()
};
fresh
.draw(|frame| draw_welcome(frame, area, &fresh_state))
.unwrap();
let buffer = terminal.backend().buffer();
assert_eq!(
buffer,
fresh.backend().buffer(),
"version={version:?}, area={area:?}"
);
if width >= 40 && height >= 23 {
if let Some(version) = version {
let notice =
find_text(buffer, &format!("magi-code {version} is available."))
.unwrap();
let invitation =
find_text(buffer, "Type a prompt below to begin.").unwrap();
assert!(notice.1 > invitation.1);
} else {
assert!(find_text(buffer, "is available.").is_none());
assert!(find_text(buffer, LOGO[0]).is_some());
}
}
}
}
}
#[test]
fn welcome_cache_matches_fresh_render_across_visual_changes() {
use crate::appearance::{ColorDepth, RuntimeAppearance};
let mut state = MissionControlState::default();
let mut cached = Terminal::new(TestBackend::new(110, 44)).unwrap();
let mut fresh = Terminal::new(TestBackend::new(110, 44)).unwrap();
for (depth, custom_theme) in [
(ColorDepth::TrueColor, false),
(ColorDepth::TrueColor, true),
(ColorDepth::Ansi256, true),
(ColorDepth::None, false),
] {
let mut appearance = RuntimeAppearance::default();
appearance.policy.depth = depth;
if custom_theme {
let mut theme = (*appearance.theme).clone();
theme.roles.surface_background = opaline::OpalineColor::new(12, 34, 56);
theme.roles.standard_styles.dimmed = opaline::OpalineStyle::new()
.with_fg(opaline::OpalineColor::new(210, 180, 150))
.with_bg(opaline::OpalineColor::new(60, 40, 20));
theme.roles.standard_styles.dimmed.italic = true;
appearance = appearance.with_resolved_theme(theme);
}
state.theme = MissionControlTheme::from_runtime_appearance(&appearance, 1);
for (width, height) in [
(100, 34),
(97, 33),
(98, 32),
(98, 33),
(78, 26),
(45, 26),
(46, 25),
(40, 23),
(1, 1),
(8, 3),
(100, 34),
] {
for (reduced_motion, color_enabled) in
[(false, true), (true, true), (false, false), (false, true)]
{
state.reduced_motion = reduced_motion;
state.color_enabled = color_enabled;
for phase in [0, 0, 12, 12, usize::MAX, 0] {
state.welcome_animation_phase = phase;
for (x, y) in [(0, 0), (3, 2)] {
let area = Rect::new(x, y, width, height);
cached
.draw(|frame| draw_welcome(frame, area, &state))
.unwrap();
fresh
.draw(|frame| draw_fresh_welcome(frame, area, &state))
.unwrap();
assert_eq!(
cached.backend().buffer(),
fresh.backend().buffer(),
"depth={depth:?}, area={area:?}, phase={phase}, reduced={reduced_motion}, color={color_enabled}"
);
}
}
}
}
}
}
#[test]
fn welcome_rain_leaves_occluded_cells_untouched_and_preserves_visible_rain() {
let mut full = Terminal::new(TestBackend::new(44, 24)).unwrap();
let mut clipped = Terminal::new(TestBackend::new(44, 24)).unwrap();
let area = Rect::new(2, 3, 40, 20);
let theme = MissionControlTheme::default();
let mut hidden_rain_cells = 0;
for opaque in [Rect::new(8, 6, 20, 10), area, Rect::default()] {
for phase in 0..40 {
full.draw(|frame| draw_rain(frame, area, Rect::default(), theme, phase))
.unwrap();
clipped
.draw(|frame| draw_rain(frame, area, opaque, theme, phase))
.unwrap();
for y in 0..24 {
for x in 0..44 {
let position = (x, y);
if opaque.contains(position.into()) {
assert_eq!(
&clipped.backend().buffer()[position],
&ratatui::buffer::Cell::default()
);
hidden_rain_cells +=
usize::from(full.backend().buffer()[position].symbol() != " ");
} else {
assert_eq!(
full.backend().buffer()[position],
clipped.backend().buffer()[position]
);
}
}
}
}
}
assert!(
hidden_rain_cells > 0,
"exercise rain that would have been overwritten"
);
}
#[test]
fn empty_transcript_shows_welcome_and_theme_colored_commands() {
let state = MissionControlState::default();
let mut terminal = Terminal::new(TestBackend::new(100, 34)).unwrap();
terminal
.draw(|frame| draw_welcome(frame, frame.area(), &state))
.unwrap();
let buffer = terminal.backend().buffer();
let tagline = find_text(buffer, "Follow the white rabbit, but take your time.").unwrap();
let logo = find_text(buffer, LOGO[0]).unwrap();
for (row_index, row) in LOGO.iter().enumerate() {
for (column, character) in row.chars().enumerate() {
assert_eq!(
buffer[(logo.0 + column as u16, logo.1 + row_index as u16)].symbol(),
character.to_string(),
"artwork row {row_index}, column {column}"
);
}
}
assert_eq!(tagline.1, logo.1 + LOGO.len() as u16 + 1);
assert!(find_text(buffer, "Type a prompt below to begin.").is_some());
let rabbit = find_text(buffer, r"(\_/)").unwrap();
assert_eq!(rabbit.1, tagline.1 + 2);
for x in tagline.0..tagline.0 + 42 {
assert_eq!(buffer[(x, tagline.1 + 1)].symbol(), " ");
}
let hotkeys = find_text(buffer, "Useful hotkeys").unwrap();
let commands = find_text(buffer, "Useful Commands").unwrap();
let last_hotkey = find_text(buffer, "[Alt-1/2]").unwrap();
assert!(hotkeys.1 < last_hotkey.1);
assert_eq!(commands.1, last_hotkey.1 + 2);
for x in commands.0..commands.0 + 50 {
assert_eq!(buffer[(x, commands.1 - 1)].symbol(), " ");
}
for (command, _) in SHORTCUTS {
let position = find_text(buffer, command).expect(command);
assert_eq!(buffer[position].fg, state.theme.text_command());
}
assert!(find_text(buffer, "Transcript will appear here").is_none());
}
#[test]
fn update_notice_is_below_welcome_and_not_in_conversation() {
let mut state = MissionControlState {
available_update_version: Some("99.0.0".into()),
..Default::default()
};
for (width, height) in [(100, 40), (80, 30), (40, 28)] {
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
terminal
.draw(|frame| draw_welcome(frame, frame.area(), &state))
.unwrap();
let buffer = terminal.backend().buffer();
let prompt = find_text(buffer, "Type a prompt below to begin.").unwrap();
let notice = find_text(buffer, "magi-code 99.0.0 is available.").unwrap();
assert!(notice.1 > prompt.1);
assert_eq!(
buffer[(notice.0, notice.1 - 1)].fg,
state.theme.text_accent()
);
}
assert!(state.transcript.is_empty());
state.transcript = vec!["Conversation content".to_string()].into();
let mut terminal = Terminal::new(TestBackend::new(100, 40)).unwrap();
terminal
.draw(|frame| draw_transcript(frame, frame.area(), &state))
.unwrap();
assert!(find_text(terminal.backend().buffer(), "99.0.0").is_none());
}
#[test]
fn compact_welcome_keeps_commands_and_yields_to_session_content() {
let mut state = MissionControlState::default();
let mut terminal = Terminal::new(TestBackend::new(40, 23)).unwrap();
terminal
.draw(|frame| draw_transcript(frame, frame.area(), &state))
.unwrap();
let buffer = terminal.backend().buffer();
for text in [
"MAGI CODE",
"/summarize-start",
"/summarize-stop",
"/update",
] {
assert!(find_text(buffer, text).is_some(), "missing {text}");
}
state.transcript = vec!["A new conversation".to_string()].into();
terminal
.draw(|frame| draw_transcript(frame, frame.area(), &state))
.unwrap();
assert!(find_text(terminal.backend().buffer(), "MAGI CODE").is_none());
assert!(find_text(terminal.backend().buffer(), "A new conversation").is_some());
}
#[test]
fn welcome_animates_logo_and_background_without_changing_help() {
let mut state = MissionControlState {
color_enabled: true,
reduced_motion: false,
..Default::default()
};
let mut terminal = Terminal::new(TestBackend::new(100, 34)).unwrap();
terminal
.draw(|frame| draw_welcome(frame, frame.area(), &state))
.unwrap();
let first = terminal.backend().buffer().clone();
state.welcome_animation_phase = 12;
terminal
.draw(|frame| draw_welcome(frame, frame.area(), &state))
.unwrap();
let second = terminal.backend().buffer();
let logo = find_text(&first, LOGO[0]).unwrap();
assert_ne!(first[logo].fg, second[logo].fg);
let bottom = (logo.0, logo.1 + LOGO.len() as u16 - 1);
assert_ne!(first[bottom].fg, second[bottom].fg);
assert!((0..34).any(|y| first[(0, y)] != second[(0, y)]));
let help = find_text(&first, "/skills").unwrap();
assert_eq!(first[help], second[help]);
for (reduced_motion, color_enabled) in [(true, true), (false, false)] {
state.reduced_motion = reduced_motion;
state.color_enabled = color_enabled;
terminal
.draw(|frame| draw_welcome(frame, frame.area(), &state))
.unwrap();
let still = terminal.backend().buffer().clone();
state.welcome_animation_phase += 1;
terminal
.draw(|frame| draw_welcome(frame, frame.area(), &state))
.unwrap();
assert_eq!(&still, terminal.backend().buffer());
assert!((0..34).all(|y| still[(0, y)].symbol() == " "));
}
}
#[test]
fn welcome_handles_tiny_and_empty_areas() {
for (width, height) in [(0, 0), (1, 1), (8, 3)] {
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
terminal
.draw(|frame| draw_welcome(frame, frame.area(), &MissionControlState::default()))
.unwrap();
}
}
}