Skip to content

feat(tui): static themed familiar cards with procedural sigils#26

Merged
BunsDev merged 1 commit into
mainfrom
feat/familiar-cards
Jun 2, 2026
Merged

feat(tui): static themed familiar cards with procedural sigils#26
BunsDev merged 1 commit into
mainfrom
feat/familiar-cards

Conversation

@BunsDev
Copy link
Copy Markdown
Member

@BunsDev BunsDev commented Jun 2, 2026

Summary

  • Replaces the walking/blinking violet mascot with a static themed card. The only motion that survives is a quarter-block eye spinner that kicks in when streaming has stalled ≥ 3s.
  • Every familiar — built-in or user-defined in ~/.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 their id, so they look the same on every machine.
  • Cards render in three surfaces: welcome panel (top-left), F2 switcher (mini-rows with per-familiar accent dots), and /agents detail view (Standard card above the persona preview).
  • Built-in palettes break the old uniform violet: kitty=violet, nova=gold, cody=cyan, charm=pink, sage=emerald, astra=indigo, echo=teal.
  • Card sizes adapt to available width: Compact (glyph only, narrow terminals), Standard (default), Large (adds role line + accent rule on wide terminals).

New modules

  • src-rust/crates/tui/src/familiar_theme.rsFamiliarTheme, FamiliarPalette, Archetype, and a stable FNV-1a id hash for procedural palette/archetype selection.
  • src-rust/crates/tui/src/familiar_card.rsrender_card(theme, size, loading), pick_size(width), render_mini_row(theme, width), and the four procedural sigil renderers.

Refactors

  • rustle.rsRustlePose collapsed to Static | Loading{frame}. All *_lines builders take a FamiliarPalette. Added archetype_lines(...) dispatcher used by the card.
  • app.rs — deleted rustle_walk_x/dir/max, rustle_temp_pose, rustle_pose_until, rustle_next_blink. tick_rustle_pose is now a 4-line static/loading toggle; rustle_look_down is a no-op kept for the Tab callsite.
  • render.rs — welcome panel + F2 switcher route through familiar_card. Hardcoded built-in emoji table is gone.
  • agents_view.rs/agents detail 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-tui clean
  • cargo build --workspace clean
  • cargo build -p claurst-tui --release clean
  • 16 new tests pass (6 familiar_theme, 5 familiar_card, 5 rustle)
  • grep -rn 'rustle_walk\|rustle_temp_pose\|rustle_next_blink\|rustle_pose_until\|RustlePose::{LookLeft,LookRight,LookDown,ArmsUp,Default}' src-rust/ → zero hits
  • Launch the TUI and confirm: no walking, no idle blink, no Tab-triggered look-down; loading spinner still appears when a request stalls
  • Cycle through built-ins with F2 — confirm each has a distinct accent palette
  • Drop a user-defined familiar in ~/.coven/familiars.toml (e.g. id = "qa", emoji = "🧪") and confirm it shows a procedural card in welcome + F2 + /agents
  • Restart the TUI multiple times — confirm the procedural familiar's palette/archetype stays identical
  • Resize the terminal narrow → Compact, default → Standard, wide → Large
  • Drop ~/.coven/assets/familiars/qa.png and confirm the image still wins on Kitty/Sixel terminals

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings June 2, 2026 20:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) and familiar_card (card layout + procedural sigils).
  • Refactor rustle to Static | 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");
@BunsDev BunsDev merged commit b339e1d into main Jun 2, 2026
1 check passed
@BunsDev BunsDev deleted the feat/familiar-cards branch June 3, 2026 01:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants