From 40d62c8f443e1538ddf872cb63a1e2c09c327248 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 23 Aug 2026 18:00:23 +0000 Subject: [PATCH] feat(tui): Warp-quiet landing and timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmark: Warp CLI. The old splash stacked three framed boxes (banner card, command table, zsh TIP) before the first prompt; the timeline stamped every line with a clock and a DBG conversation-id row. - Splash: drop frames/gauge/tabs entirely. Figlet art, one dim meta line (version · agents · font), agent flock chips greedily wrapped to terminal width so all 17 stay visible. - Command sheet: one dim hint line (/ for commands · : agents · ! for shell) replaces the framed table; zsh TIP box removed. - Timeline: no per-line clock, dim gutter; plain mode loses its clock too. Chips keep TOOL/SKIL/AGNT/ACT/DONE lanes. - REPL: DBG 'Initialize ' conversation rows no longer print. - Spinner: '· Ctrl+C to interrupt' suffix dropped — tick + elapsed + label only. Co-Authored-By: AimeeCodes --- crates/aimee_main/src/banner.rs | 313 +++++++------------------ crates/aimee_main/src/title_display.rs | 26 +- crates/aimee_main/src/ui.rs | 12 +- crates/aimee_spinner/src/lib.rs | 6 +- 4 files changed, 95 insertions(+), 262 deletions(-) diff --git a/crates/aimee_main/src/banner.rs b/crates/aimee_main/src/banner.rs index ad4be78..4c230b9 100644 --- a/crates/aimee_main/src/banner.rs +++ b/crates/aimee_main/src/banner.rs @@ -1,4 +1,4 @@ -use std::{fmt, io}; +use std::io; use aimee_tracker::VERSION; use colored::Colorize; @@ -6,12 +6,11 @@ use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, BorderType, Fill, LineGauge, Paragraph, Row, Table, Tabs, Widget}; +use ratatui::widgets::{Fill, Paragraph, Widget}; use crate::theme; const BANNER: &str = include_str!("banner"); -const TAGLINE: &str = "CLI agent flock · 17 specialists · Warp palette"; /// Built-in agent-switch chips (loop + FE / BE / PLAT roster). /// Keep in sync with `crates/aimee_repo/src/agents/*.md`. @@ -44,48 +43,10 @@ pub fn agent_chip_count() -> usize { CHIPS.len() } -/// Renders messages into a styled box with border characters. -struct DisplayBox { - messages: Vec, -} - -impl DisplayBox { - /// Creates a new Box with the given messages. - fn new(messages: Vec) -> Self { - Self { messages } - } -} - -impl fmt::Display for DisplayBox { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let visible_len = |s: &str| console::measure_text_width(s); - let width: usize = self - .messages - .iter() - .map(|s| visible_len(s)) - .max() - .unwrap_or(0) - + 4; - let top = format!("┌{}┐", "─".repeat(width.saturating_sub(2))); - let bottom = format!("└{}┘", "─".repeat(width.saturating_sub(2))); - let fmt_line = |s: &str| { - let padding = width.saturating_sub(4).saturating_sub(visible_len(s)); - format!("│ {}{} │", s, " ".repeat(padding)) - }; - - writeln!(f, "{}", top)?; - for msg in &self.messages { - writeln!(f, "{}", fmt_line(msg))?; - } - write!(f, "{}", bottom) - } -} - /// Displays the banner with version and command tips. /// -/// The art, tagline, and slash-command chips are composed in a ratatui -/// [`Buffer`] then flushed as ANSI so the live path uses the same renderer as -/// future full-screen TUI surfaces. +/// Warp-quiet landing: figlet art, one dim meta line, one compact command +/// hint line, one agent-flock line. No frames, gauges, or tab bars. /// /// # Arguments /// @@ -107,10 +68,6 @@ pub fn display(cli_mode: bool) -> io::Result<()> { print_command_sheet(cli_mode); - if !cli_mode { - display_zsh_encouragement(); - } - Ok(()) } @@ -122,7 +79,7 @@ fn print_ratatui_splash() { print!("{}", buffer_to_ansi(&buf)); } -/// Area large enough for the framed card: art + gauge + tabs + agent rows + tagline. +/// Area for the quiet landing: art + meta line + width-wrapped flock rows. fn splash_area() -> Rect { let art_width = BANNER .lines() @@ -134,185 +91,101 @@ fn splash_area() -> Rect { .lines() .filter(|line| !line.trim().is_empty()) .count() as u16; - // art + gauge + loop tabs + 3 agent chip rows + tagline + font hint - Rect::new(0, 0, width, art_lines.saturating_add(9)) + // art + meta + up to 5 flock rows (17 agents greedily wrap at ~82 cols). + Rect::new(0, 0, width, art_lines.saturating_add(6)) } -/// Paints a framed ratatui card: void fill, rounded Block, figlet art, -/// loop LineGauge, full agent flock chips, and the tagline. +/// Splits chips into rows that fit `width` columns, honoring each chip's +/// rendered size (` key `, ` label `, 2-col gap). +fn wrap_chips(width: u16) -> Vec> { + let limit = width as usize; + let mut rows: Vec> = Vec::new(); + let mut current: Vec<(String, String)> = Vec::new(); + let mut used = 0usize; + for (key, label) in CHIPS { + let chip_width = key.chars().count() + 2 + label.chars().count() + 2 + 2; + if !current.is_empty() && used + chip_width > limit { + rows.push(std::mem::take(&mut current)); + used = 0; + } + used += chip_width; + current.push(((*key).to_string(), (*label).to_string())); + } + if !current.is_empty() { + rows.push(current); + } + rows +} + +/// Paints the Warp-quiet landing: art, one dim meta row, wrapped flock rows. pub fn render_splash(buf: &mut Buffer, area: Rect) { Fill::new(" ") .style(Style::default().bg(theme::palette::RATATUI_VOID)) .render(area, buf); - let frame = Block::bordered() - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(theme::palette::RATATUI_CYAN)) - .title(Line::from(vec![ - Span::styled(" 🍑 ", theme::button_key_style()), - Span::styled( - "Aimee Codes", - Style::default() - .fg(theme::palette::RATATUI_CYAN) - .add_modifier(Modifier::BOLD), - ), - Span::styled(" 🍑 ", theme::button_key_style()), - ])) - .title( - Line::from(format!(" v{VERSION} ")) - .right_aligned() - .style(Style::default().fg(theme::palette::RATATUI_GOLD)), - ) - .title_bottom( - Line::from(format!( - " {} agents · font: {} ", - CHIPS.len(), - theme::WARP_FONT_FACE - )) - .centered() - .style( - Style::default() - .fg(theme::palette::RATATUI_VIOLET) - .add_modifier(Modifier::BOLD), - ), - ); - let inner = frame.inner(area); - frame.render(area, buf); - let art_lines = BANNER .lines() .filter(|line| !line.trim().is_empty()) .count() as u16; + let flock_rows = wrap_chips(area.width); let chunks = Layout::vertical([ Constraint::Length(art_lines), - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(3), // agent chip rows - Constraint::Min(1), + Constraint::Length(1), // meta + Constraint::Length(flock_rows.len() as u16), ]) - .split(inner); + .split(area); if !chunks.is_empty() { render_into(buf, chunks[0]); } if chunks.len() > 1 { - LineGauge::default() - .filled_symbol("━") - .unfilled_symbol("─") - .ratio(1.0) - .label(Line::from(Span::styled( - "LOOP ", + Paragraph::new(Line::from(vec![ + Span::styled( + format!("v{VERSION}"), Style::default() - .fg(theme::palette::RATATUI_LIME) + .fg(theme::palette::RATATUI_GOLD) .add_modifier(Modifier::BOLD), - ))) - .filled_style(Style::default().fg(theme::palette::RATATUI_CYAN)) - .unfilled_style(Style::default().fg(theme::palette::RATATUI_VIOLET)) - .render(chunks[1], buf); + ), + Span::styled( + format!( + " · {} agents · font: {}", + CHIPS.len(), + theme::WARP_FONT_FACE + ), + Style::default().fg(theme::palette::RATATUI_MUTED), + ), + ])) + .render(chunks[1], buf); } if chunks.len() > 2 { - let specialist_tab = format!("+{} specialists", CHIPS.len().saturating_sub(3)); - Tabs::new([ - ":sage research".to_string(), - ":muse plan".to_string(), - ":aimee implement".to_string(), - specialist_tab, - ]) - .select(2) - .highlight_style(theme::button_key_style().bg(theme::palette::RATATUI_LIME)) - .style(Style::default().fg(theme::palette::RATATUI_VIOLET)) - .divider("·") - .render(chunks[2], buf); - } - if chunks.len() > 3 { - // Three rows of agent chips so the full flock is visible. - let chip_area = chunks[3]; - let rows = Layout::vertical([ - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), - ]) - .split(chip_area); - let per_row = CHIPS.len().div_ceil(3); - for (row_idx, row) in rows.iter().enumerate() { - let start = row_idx * per_row; - let end = (start + per_row).min(CHIPS.len()); - if start < end { - render_chips_slice(buf, row.x, row.y, row.width, &CHIPS[start..end], start); - } + let rows = Layout::vertical( + std::iter::repeat_n(Constraint::Length(1), flock_rows.len()).collect::>(), + ) + .split(chunks[2]); + let mut offset = 0; + for (row, chips) in rows.iter().zip(&flock_rows) { + let owned: Vec<(&str, &str)> = chips + .iter() + .map(|(k, l)| (k.as_str(), l.as_str())) + .collect(); + render_chips_slice(buf, row.x, row.y, row.width, &owned, offset); + offset += chips.len(); } } - if chunks.len() > 4 { - Paragraph::new(Line::from(Span::styled( - TAGLINE, - Style::default() - .fg(theme::palette::RATATUI_LIME) - .add_modifier(Modifier::BOLD), - ))) - .render(chunks[4], buf); - } } -/// Command cheatsheet under the splash, rendered as a ratatui table. -fn print_command_sheet(cli_mode: bool) { - let rows: Vec<(&str, &str)> = if cli_mode { - vec![ - ("/ or : then Tab", "command menu (all slash cmds)"), - (":new", "new conversation"), - (":info :model :provider", "session / model / provider"), - (":aimee :muse :sage", "loop roles"), - (":fe-* :be-* :plat-*", "15 specialist agents"), - (":tpl-*", "prompt templates"), - ("/review /incident /ship", "enterprise prompt packs"), - ] - } else { - vec![ - ("/ or : then Tab", "open command palette"), - (":new :info :usage :help", "get started"), - (":model :provider", "model / provider"), - (":aimee :muse :sage", "loop · implement / plan / research"), - (":fe-ui :be-api :plat-k8s …", "full specialist flock"), - (":tpl-debug :tpl-handoff …", "prompt templates"), - ("/review /harden /ship /oncall", "enterprise commands"), - (":update :exit", "self-update · quit (Ctrl+D)"), - ] - }; - let height = (rows.len() as u16).saturating_add(2); - let width = 78u16; - let area = Rect::new(0, 0, width, height); - let mut buf = Buffer::empty(area); - Fill::new(" ") - .style(Style::default().bg(theme::palette::RATATUI_VOID)) - .render(area, &mut buf); - let table_rows = rows.into_iter().map(|(cmd, hint)| { - Row::new([ - Line::from(Span::styled( - cmd, - Style::default() - .fg(theme::palette::RATATUI_GOLD) - .add_modifier(Modifier::BOLD), - )), - Line::from(Span::styled( - hint, - Style::default().fg(theme::palette::RATATUI_CYAN), - )), - ]) - }); - Table::new(table_rows, [Constraint::Length(28), Constraint::Min(20)]) - .block( - Block::bordered() - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(theme::palette::RATATUI_VIOLET)) - .title(Line::from(Span::styled( - " commands · Warp palette ", - Style::default() - .fg(theme::palette::RATATUI_GOLD) - .add_modifier(Modifier::BOLD), - ))) - .style(Style::default().bg(theme::palette::RATATUI_VOID)), - ) - .render(area, &mut buf); - print!("{}", buffer_to_ansi(&buf)); +/// Compact command hints printed under the splash. +/// +/// Warp keeps help to a single dim line (`⌘ / for commands`); we match that +/// with one quiet row instead of a framed table. +fn print_command_sheet(_cli_mode: bool) { + let hint = format!( + "{} for commands · {} agents · {} for shell", + "/".truecolor(0xFF, 0xCC, 0x02), + ":".truecolor(0xFF, 0xCC, 0x02), + "!".truecolor(0xBF, 0x7A, 0xF0), + ); + println!("{}", hint.dimmed()); println!(); } @@ -436,30 +309,6 @@ pub fn render_into(buf: &mut Buffer, area: Rect) { } } -/// Encourages users to use the zsh plugin for a better experience. -fn display_zsh_encouragement() { - let tip = DisplayBox::new(vec![ - format!( - "{} {}", - "TIP:".bold().truecolor(255, 210, 0), - "For the best experience, use our zsh plugin!".bold() - ), - format!( - "{} {} {}", - "·".dimmed(), - "Set up Aimee Codes via our zsh plugin:".dimmed(), - "aimee zsh setup".bold().truecolor(163, 255, 18), - ), - format!( - "{} {} {}", - "·".dimmed(), - "Learn more:".dimmed(), - "https://aimeecodes.dev/docs/zsh-support".truecolor(0, 229, 255) - ), - ]); - println!("{}", tip); -} - /// Invisible widget wrapper so the banner can sit on a ratatui surface. pub struct BannerWidget; @@ -509,20 +358,23 @@ mod tests { } #[test] - fn test_render_splash_includes_tagline_and_aimee_chip() { + fn test_render_splash_quiet_landing() { let area = splash_area(); let mut buf = Buffer::empty(area); render_splash(&mut buf, area); let actual = buffer_text(&buf); - assert!(actual.contains("CLI agent flock")); - assert!(actual.contains("Aimee Codes")); - assert!(actual.contains("🍑")); - assert!(actual.contains("LOOP")); + // Meta line + two flock rows; no frame chrome. + assert!(actual.contains("agents")); + assert!(actual.contains("JetBrains Mono")); + // Row 1 leads with the loop trio. assert!(actual.contains(":aimee")); assert!(actual.contains(":muse")); assert!(actual.contains(":sage")); + // Row 2 carries the tail of the flock (17 agents split across 2 rows). + assert!(actual.contains(":plat-sre") || actual.contains(":plat-cloud")); assert!(actual.contains(":fe-ui") || actual.contains("fe-ui")); - assert!(actual.contains("specialists") || actual.contains("agents")); + assert!(!actual.contains('╭')); + assert!(!actual.contains('╰')); assert_eq!(agent_chip_count(), 17); } @@ -534,7 +386,6 @@ mod tests { let actual = buffer_to_ansi(&buf); assert!(actual.contains("\u{1b}[38;2;")); assert!(actual.contains(":aimee")); - assert!(actual.contains(TAGLINE)); } #[test] diff --git a/crates/aimee_main/src/title_display.rs b/crates/aimee_main/src/title_display.rs index b4eb98a..420b247 100644 --- a/crates/aimee_main/src/title_display.rs +++ b/crates/aimee_main/src/title_display.rs @@ -10,7 +10,6 @@ use std::fmt; use aimee_domain::{Category, TitleFormat}; -use chrono::Local; use nu_ansi_term::Style; use crate::theme::palette; @@ -97,12 +96,7 @@ impl TitleDisplay { fn format_with_colors(&self) -> String { let (label, chip_style) = self.chip(); - let local_time: chrono::DateTime = self.inner.timestamp.into(); - let gutter = Style::new().fg(palette::VIOLET).paint("│"); - let clock = Style::new() - .fg(palette::VIOLET) - .dimmed() - .paint(format!("{}", local_time.format("%H:%M:%S"))); + let gutter = Style::new().fg(palette::VIOLET).dimmed().paint("│"); let title = match self.inner.category { Category::Error => Style::new() @@ -145,11 +139,12 @@ impl TitleDisplay { .to_string(), }; - let mut buf = format!( - "{gutter} {} {} {title}", - chip_style.paint(format!(" {label} ")), - clock, - ); + // Warp-quiet: no clock on every line. The chip carries the kind; the + // title carries the payload. + let mut buf = format!("{gutter} {}", chip_style.paint(format!(" {label} "))); + + buf.push_str(" "); + buf.push_str(&title); if let Some(ref sub_title) = self.inner.sub_title { // Agent handoffs get an arrow (Super Grok Heavy style hop). @@ -180,12 +175,7 @@ impl TitleDisplay { fn format_plain(&self) -> String { let label = self.plain_chip(); - let local_time: chrono::DateTime = self.inner.timestamp.into(); - let mut buf = format!( - "| [{label}] [{}] {}", - local_time.format("%H:%M:%S"), - self.inner.title - ); + let mut buf = format!("| [{label}] {}", self.inner.title); if let Some(ref sub_title) = self.inner.sub_title { if matches!(self.inner.category, Category::Agent) { buf.push_str(&format!(" → {sub_title}")); diff --git a/crates/aimee_main/src/ui.rs b/crates/aimee_main/src/ui.rs index 4bc590a..e7bcbfa 100644 --- a/crates/aimee_main/src/ui.rs +++ b/crates/aimee_main/src/ui.rs @@ -4586,15 +4586,9 @@ impl A + Send + Sync> UI new_conversation: bool, id: ConversationId, ) -> Result<(), anyhow::Error> { - let mut title = if new_conversation { - "Initialize".to_string() - } else { - "Continue".to_string() - }; - - title.push_str(format!(" {}", id.into_string()).as_str()); - - self.writeln_title(TitleFormat::debug(title))?; + // Warp-quiet: the conversation id is metadata, not a timeline event. + // Keep it out of the visual flow but leave it available via :info. + let _ = (new_conversation, id); Ok(()) } diff --git a/crates/aimee_spinner/src/lib.rs b/crates/aimee_spinner/src/lib.rs index 3498d42..70594da 100644 --- a/crates/aimee_spinner/src/lib.rs +++ b/crates/aimee_spinner/src/lib.rs @@ -46,21 +46,19 @@ fn styled_loader_line( terminal_width: usize, ) -> String { let elapsed = format_elapsed_time(elapsed); - let suffix = "· Ctrl+C to interrupt"; let max_width = terminal_width .saturating_sub(WRAP_GUARD_COLUMNS) .max(MIN_TERMINAL_WIDTH); let tick = tick.green().to_string(); let elapsed = elapsed.white().to_string(); - let suffix = suffix.white().dimmed().to_string(); - let fixed = format!("{tick} {elapsed} {suffix}"); + let fixed = format!("{tick} {elapsed}"); let message_width = max_width.saturating_sub(visible_width(&fixed)).max(1); let message = truncate_to_visible_width(message, message_width) .green() .bold() .to_string(); - let styled = format!("{tick} {message} {elapsed} {suffix}"); + let styled = format!("{tick} {message} {elapsed}"); truncate_to_visible_width(&styled, max_width) }