feat(agents): give Grok its brand avatar

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.
This commit is contained in:
l0ng-ai
2026-07-25 20:17:24 +08:00
parent c76ef87d02
commit eced0af754
3 changed files with 36 additions and 5 deletions
+5
View File
@@ -0,0 +1,5 @@
<!-- Grok brand mark, from lobehub/lobe-icons (MIT, Copyright (c) 2023 LobeHub).
Normalized to tty7's 24x24 icon format; geometry unchanged. -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 874 B

+11 -5
View File
@@ -338,8 +338,11 @@ impl CLIAgent {
/// Brand accent (0xRRGGBB) for the tab chip's agent dot. Chosen for legibility /// 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 /// 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 /// *white* field vanishes against a light theme, so vendors whose mark is a
/// a recognizable mid-tone hue instead; Codex keeps its black field. /// 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 { pub fn accent_rgb(self) -> u32 {
match self { match self {
CLIAgent::Claude => 0xD97757, // Claude terracotta CLIAgent::Claude => 0xD97757, // Claude terracotta
@@ -357,7 +360,7 @@ impl CLIAgent {
CLIAgent::Hermes => 0x8B5CF6, // violet CLIAgent::Hermes => 0x8B5CF6, // violet
CLIAgent::Vibe => 0xFF7000, // Mistral orange CLIAgent::Vibe => 0xFF7000, // Mistral orange
CLIAgent::Antigravity => 0x2563EB, // Google blue (darker than Gemini's) 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 CLIAgent::Qwen => 0x7C3AED, // Qwen purple
} }
} }
@@ -379,6 +382,7 @@ impl CLIAgent {
CLIAgent::Cursor => "icons/agents/cursor.svg", CLIAgent::Cursor => "icons/agents/cursor.svg",
CLIAgent::Goose => "icons/agents/goose.svg", CLIAgent::Goose => "icons/agents/goose.svg",
CLIAgent::Droid => "icons/agents/droid.svg", CLIAgent::Droid => "icons/agents/droid.svg",
CLIAgent::Grok => "icons/agents/grok.svg",
// No brand mark bundled → generic robot glyph. // No brand mark bundled → generic robot glyph.
CLIAgent::Aider CLIAgent::Aider
| CLIAgent::Pi | CLIAgent::Pi
@@ -386,7 +390,6 @@ impl CLIAgent {
| CLIAgent::Hermes | CLIAgent::Hermes
| CLIAgent::Vibe | CLIAgent::Vibe
| CLIAgent::Antigravity | CLIAgent::Antigravity
| CLIAgent::Grok
| CLIAgent::Qwen => "icons/bot.svg", | 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] #[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::Codex.accent_rgb(), 0x000000);
assert_eq!(CLIAgent::Grok.accent_rgb(), 0x000000);
} }
#[test] #[test]
+20
View File
@@ -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/cursor.svg" => include_bytes!("../../assets/icons/agents/cursor.svg"),
"icons/agents/goose.svg" => include_bytes!("../../assets/icons/agents/goose.svg"), "icons/agents/goose.svg" => include_bytes!("../../assets/icons/agents/goose.svg"),
"icons/agents/droid.svg" => include_bytes!("../../assets/icons/agents/droid.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, _ => return None,
}; };
Some(bytes) 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 — /// A `stock/` path for a glyph tty7 never overrode still has to resolve —
/// the prefix is a bypass, not a separate asset set. /// the prefix is a bypass, not a separate asset set.
#[test] #[test]