From eced0af754c20033c3d2e9ed334be4ae96abf1d9 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:17:24 +0800 Subject: [PATCH] feat(agents): give Grok its brand avatar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grok was drawing the generic robot glyph on a slate disc — the fallback picked back when no usable mark was bundled. xAI publishes its symbol only as a ~2:1 landscape lockup that bleeds off its own canvas; traced and fitted to a 24x24 box it is unreadable as a 16px silhouette, which is what the tab chip and sidebar render. So the bundled mark is lobehub/lobe-icons' square transcription (MIT), drawn for exactly this avatar use. Its notice rides in the SVG. The slate accent goes with it. That mid-tone exists for vendors whose monochrome mark is grey or a gradient (Cursor), because a white field vanishes on a light theme; a black field has no such problem — it stays darker than even the darkest theme background and the white mark carries the badge. Grok brands in black, like Codex, so it keeps that. Adds a guard test: every CLIAgent::icon_path must resolve through the asset source. A brand mark means touching two files, and forgetting the registration costs the agent its avatar silently. --- assets/icons/agents/grok.svg | 5 +++++ src/core/cli_agent.rs | 16 +++++++++++----- src/ui/assets.rs | 20 ++++++++++++++++++++ 3 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 assets/icons/agents/grok.svg diff --git a/assets/icons/agents/grok.svg b/assets/icons/agents/grok.svg new file mode 100644 index 00000000..82745602 --- /dev/null +++ b/assets/icons/agents/grok.svg @@ -0,0 +1,5 @@ + + + + diff --git a/src/core/cli_agent.rs b/src/core/cli_agent.rs index 291e2d71..70fa4b79 100644 --- a/src/core/cli_agent.rs +++ b/src/core/cli_agent.rs @@ -338,8 +338,11 @@ impl CLIAgent { /// Brand accent (0xRRGGBB) for the tab chip's agent dot. Chosen for legibility /// on both light and dark themes rather than exact brand black/white. A pure - /// black or white dot vanishes against one theme, so monochrome vendors get - /// a recognizable mid-tone hue instead; Codex keeps its black field. + /// *white* field vanishes against a light theme, so vendors whose mark is a + /// grey or gradient monochrome (Cursor) get a recognizable mid-tone hue + /// instead. A black field is a different case: it stays darker than even the + /// darkest theme background and the white mark on it carries the badge, so + /// vendors who actually brand in black (Codex, Grok) keep it. pub fn accent_rgb(self) -> u32 { match self { CLIAgent::Claude => 0xD97757, // Claude terracotta @@ -357,7 +360,7 @@ impl CLIAgent { CLIAgent::Hermes => 0x8B5CF6, // violet CLIAgent::Vibe => 0xFF7000, // Mistral orange CLIAgent::Antigravity => 0x2563EB, // Google blue (darker than Gemini's) - CLIAgent::Grok => 0x64748B, // xAI is monochrome → slate + CLIAgent::Grok => 0x000000, // xAI brands in black CLIAgent::Qwen => 0x7C3AED, // Qwen purple } } @@ -379,6 +382,7 @@ impl CLIAgent { CLIAgent::Cursor => "icons/agents/cursor.svg", CLIAgent::Goose => "icons/agents/goose.svg", CLIAgent::Droid => "icons/agents/droid.svg", + CLIAgent::Grok => "icons/agents/grok.svg", // No brand mark bundled → generic robot glyph. CLIAgent::Aider | CLIAgent::Pi @@ -386,7 +390,6 @@ impl CLIAgent { | CLIAgent::Hermes | CLIAgent::Vibe | CLIAgent::Antigravity - | CLIAgent::Grok | CLIAgent::Qwen => "icons/bot.svg", } } @@ -940,9 +943,12 @@ mod tests { } } + /// The two vendors who actually brand in black keep the black field rather + /// than the mid-tone substitute monochrome marks otherwise get. #[test] - fn codex_avatar_uses_its_black_brand_field() { + fn black_branded_avatars_keep_their_brand_field() { assert_eq!(CLIAgent::Codex.accent_rgb(), 0x000000); + assert_eq!(CLIAgent::Grok.accent_rgb(), 0x000000); } #[test] diff --git a/src/ui/assets.rs b/src/ui/assets.rs index 05b12d72..d2dd4cbf 100644 --- a/src/ui/assets.rs +++ b/src/ui/assets.rs @@ -151,6 +151,11 @@ fn agent_icon(path: &str) -> Option<&'static [u8]> { "icons/agents/cursor.svg" => include_bytes!("../../assets/icons/agents/cursor.svg"), "icons/agents/goose.svg" => include_bytes!("../../assets/icons/agents/goose.svg"), "icons/agents/droid.svg" => include_bytes!("../../assets/icons/agents/droid.svg"), + // The one mark not taken from the vendor directly: xAI publishes its + // symbol only as a ~2:1 landscape lockup that turns to mush as a 16px + // silhouette, so this is lobehub/lobe-icons' square transcription (MIT), + // drawn for exactly this avatar use. Its notice rides in the SVG. + "icons/agents/grok.svg" => include_bytes!("../../assets/icons/agents/grok.svg"), _ => return None, }; Some(bytes) @@ -181,6 +186,21 @@ mod tests { } } + /// Every agent avatar must resolve to real bytes. Adding a brand mark means + /// touching two files — the SVG and the arm above — and forgetting the + /// second one costs the agent its avatar with nothing to show for it. + #[test] + fn every_agent_icon_resolves() { + for agent in crate::core::cli_agent::CLIAgent::ALL { + let path = agent.icon_path(); + assert!( + Assets.load(path).unwrap().is_some(), + "{} points at {path}, which nothing serves", + agent.display_name() + ); + } + } + /// A `stock/` path for a glyph tty7 never overrode still has to resolve — /// the prefix is a bypass, not a separate asset set. #[test]