feat(tui): static themed familiar cards with procedural sigils#26
Merged
Conversation
Replace the walking/blinking violet mascot with a static themed card so
every familiar — built-in or user-defined in ~/.coven/familiars.toml —
gets first-class visual identity in the welcome panel, F2 switcher, and
/agents detail view.
- familiar_theme: per-familiar palette + archetype. Built-ins get hand-
tuned colours (kitty=violet, nova=gold, cody=cyan, charm=pink,
sage=emerald, astra=indigo, echo=teal). User-defined entries get a
deterministic palette + sigil archetype hashed from their id so they
stay stable across sessions and machines.
- familiar_card: composes Compact/Standard/Large cards plus a mini-row
for the F2 switcher; four procedural sigil frames (crystal, hex,
rune, seal) host the emoji for any user-defined familiar.
- rustle: collapses RustlePose to Static | Loading{frame} and threads
the palette through every glyph builder. The only motion that
survives is the eye-spinner during stalled streaming.
- app: drops rustle_walk_*, rustle_temp_pose, rustle_pose_until,
rustle_next_blink; tick_rustle_pose is now a 4-line static/loading
toggle, and rustle_look_down is a no-op for the Tab callsite.
- render: welcome panel + F2 switcher route through familiar_card; the
hardcoded built-in emoji table is gone.
- agents_view: familiar-sourced agents show a Standard card above the
persona preview.
- docs/familiars.md: rewrites the glyph section to describe the new
static themed cards and procedural sigils.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors the TUI’s familiar/mascot rendering from an animated “rustle” glyph to static, themed familiar cards, including deterministic procedural sigils for user-defined familiars loaded from ~/.coven/familiars.toml. It introduces a theming layer (palette + archetype) and routes the welcome panel, F2 familiar switcher, and /agents detail view through the new card renderer, while keeping only a loading eye-spinner animation when streaming is stalled.
Changes:
- Add
familiar_theme(palette/archetype resolution + stable FNV-1a hash) andfamiliar_card(card layout + procedural sigils). - Refactor
rustletoStatic | Loading { frame }and add palette-aware archetype dispatch for built-ins. - Update TUI surfaces (welcome, F2 switcher,
/agents) and docs to use the new static card UI.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src-rust/crates/tui/src/rustle.rs | Collapses pose enum to static/loading; makes glyph builders palette-aware; adds archetype dispatcher. |
| src-rust/crates/tui/src/render.rs | Routes welcome panel + familiar switcher to familiar_card/familiar_theme. |
| src-rust/crates/tui/src/lib.rs | Exposes new familiar_theme and familiar_card modules. |
| src-rust/crates/tui/src/familiar_theme.rs | Implements built-in palettes and deterministic procedural theming for user familiars. |
| src-rust/crates/tui/src/familiar_card.rs | Implements card sizing/layout, mini-row rendering, and procedural sigil renderers. |
| src-rust/crates/tui/src/app.rs | Removes walk/blink pose logic; simplifies pose ticking to stalled-loading spinner only. |
| src-rust/crates/tui/src/agents_view.rs | Renders a familiar card in /agents detail for familiar-sourced agents. |
| docs/familiars.md | Updates documentation to describe static themed cards + procedural sigils and where they appear. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+77
to
+80
| if let Some(role) = &theme.role { | ||
| spans.push(Span::raw(" ")); | ||
| spans.push(Span::styled(truncate(role, (width as usize).saturating_sub(theme.display_name.len() + 10)), muted)); | ||
| } |
Comment on lines
1532
to
+1535
| let familiar_name = app.config.familiar.as_deref().unwrap_or("kitty"); | ||
| let familiar_label = format!("familiar: {}", familiar_name); | ||
| let daemon_familiars = claurst_core::coven_shared::load_familiars().unwrap_or_default(); | ||
| let theme = familiar_theme::resolve(familiar_name, &daemon_familiars); | ||
| let card_size = familiar_card::pick_size(left_w); |
Comment on lines
+1022
to
+1026
| if is_familiar { | ||
| if let Some(id) = def.source.strip_prefix("coven:familiar:") { | ||
| let daemon = coven_shared::load_familiars().unwrap_or_default(); | ||
| let theme = familiar_theme::resolve(id, &daemon); | ||
| for line in familiar_card::render_card(&theme, CardSize::Standard, None) { |
Comment on lines
+36
to
+43
| pub fn pick_size(width: u16) -> CardSize { | ||
| if width < 20 { | ||
| CardSize::Compact | ||
| } else if width < 28 { | ||
| CardSize::Standard | ||
| } else { | ||
| CardSize::Large | ||
| } |
Comment on lines
+319
to
+333
| fn truncate(s: &str, max: usize) -> String { | ||
| if s.chars().count() <= max { | ||
| s.to_string() | ||
| } else { | ||
| let mut out = String::new(); | ||
| for (i, c) in s.chars().enumerate() { | ||
| if i + 1 >= max { | ||
| out.push('\u{2026}'); | ||
| break; | ||
| } | ||
| out.push(c); | ||
| } | ||
| out | ||
| } | ||
| } |
Comment on lines
+120
to
+134
| fn title_line(theme: &FamiliarTheme, primary: Color, inner_w: u16) -> Line<'static> { | ||
| let dot = Span::styled("\u{25cf}", Style::default().fg(theme.access_color())); | ||
| let name = Span::styled( | ||
| theme.display_name.clone(), | ||
| Style::default().fg(primary).add_modifier(Modifier::BOLD), | ||
| ); | ||
| let title_prefix = Span::styled("\u{256d} ".to_string(), Style::default().fg(primary)); | ||
| let title_gap = Span::raw(" "); | ||
| // Approx title width: "╭ " (2) + dot (1) + " " + name + " ". Pad fill to inner_w. | ||
| let used = 2 + 1 + 1 + theme.display_name.chars().count() + 1; | ||
| let fill = (inner_w as usize).saturating_sub(used) + 1; // +1 to land on the corner | ||
| let fill_str = "\u{2500}".repeat(fill); | ||
| let suffix = Span::styled(format!("{}\u{256e}", fill_str), Style::default().fg(primary)); | ||
| Line::from(vec![title_prefix, dot, Span::raw(" "), name, title_gap, suffix]) | ||
| } |
Comment on lines
+302
to
+317
| fn visible_str_width(s: &str) -> usize { | ||
| s.chars() | ||
| .map(|c| { | ||
| let cp = c as u32; | ||
| if cp >= 0x1F300 && cp <= 0x1FAFF { | ||
| 2 | ||
| } else if (0x2600..=0x27BF).contains(&cp) { | ||
| // Misc symbols / dingbats, usually 1 cell but emoji-style | ||
| // (✨, ★, ✦, etc.) commonly render as 1 cell in modern terminals. | ||
| 1 | ||
| } else { | ||
| 1 | ||
| } | ||
| }) | ||
| .sum() | ||
| } |
Comment on lines
+3034
to
3036
| let daemon_familiars = claurst_core::coven_shared::load_familiars().unwrap_or_default(); | ||
| let interior_w = popup_w.saturating_sub(2); | ||
|
|
Comment on lines
+285
to
+289
| /// Legacy entry point — resolve a familiar slug to its theme and render the | ||
| /// glyph block. Keeps the old function name so any straggling caller can | ||
| /// keep working; new code should go through [`crate::familiar_card::render_card`]. | ||
| pub fn rustle_lines_for(familiar: Option<&str>, pose: &RustlePose) -> [Line<'static>; 5] { | ||
| let id = familiar.unwrap_or("kitty"); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
~/.coven/familiars.toml— gets a quality card with its own accent palette. User-defined entries get a procedurally generated sigil (crystal / hex / rune / seal) hashed deterministically from theirid, so they look the same on every machine./agentsdetail view (Standard card above the persona preview).kitty=violet,nova=gold,cody=cyan,charm=pink,sage=emerald,astra=indigo,echo=teal.New modules
src-rust/crates/tui/src/familiar_theme.rs—FamiliarTheme,FamiliarPalette,Archetype, and a stable FNV-1a id hash for procedural palette/archetype selection.src-rust/crates/tui/src/familiar_card.rs—render_card(theme, size, loading),pick_size(width),render_mini_row(theme, width), and the four procedural sigil renderers.Refactors
rustle.rs—RustlePosecollapsed toStatic | Loading{frame}. All*_linesbuilders take aFamiliarPalette. Addedarchetype_lines(...)dispatcher used by the card.app.rs— deletedrustle_walk_x/dir/max,rustle_temp_pose,rustle_pose_until,rustle_next_blink.tick_rustle_poseis now a 4-line static/loading toggle;rustle_look_downis a no-op kept for the Tab callsite.render.rs— welcome panel + F2 switcher route throughfamiliar_card. Hardcoded built-in emoji table is gone.agents_view.rs—/agentsdetail panel shows the Standard card above the persona text for familiar-sourced agents.docs/familiars.md— rewrites the glyph section to describe the new static themed cards and procedural sigils.Test plan
cargo build -p claurst-tuicleancargo build --workspacecleancargo build -p claurst-tui --releasecleanfamiliar_theme, 5familiar_card, 5rustle)grep -rn 'rustle_walk\|rustle_temp_pose\|rustle_next_blink\|rustle_pose_until\|RustlePose::{LookLeft,LookRight,LookDown,ArmsUp,Default}' src-rust/→ zero hits~/.coven/familiars.toml(e.g.id = "qa", emoji = "🧪") and confirm it shows a procedural card in welcome + F2 +/agents~/.coven/assets/familiars/qa.pngand confirm the image still wins on Kitty/Sixel terminals🤖 Generated with Claude Code