feat(#1756): Add dynamic agent color styling system#1757
feat(#1756): Add dynamic agent color styling system#1757aheritier wants to merge 5 commits intodocker:mainfrom
Conversation
|
Very cool idea, I just wonder how well this plays with our theme system? |
8e5f912 to
cebba6a
Compare
There was a problem hiding this comment.
Pull request overview
Adds a dynamic, deterministic agent color system to the TUI so agent badges and sidebar accents are derived from a fixed palette based on the agent’s order (as reported by the runtime team info).
Changes:
- Introduces badge and accent color palettes plus helper APIs to render agent-specific styles.
- Tracks agent ordering via a shared registry updated from
TeamInfoEvent/SetAvailableAgents. - Updates multiple TUI components to render agent badges/names using the new dynamic style functions.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/tui/styles/agent_colors.go | Implements palettes, agent→index registry, and dynamic badge/accent style helpers. |
| pkg/tui/styles/agent_colors_test.go | Adds tests for agent ordering/indexing and determinism of generated styles. |
| pkg/tui/service/sessionstate.go | Updates agent-order registry when available agents are set from runtime events. |
| pkg/tui/components/tool/transfertask/transfertask.go | Switches to dynamic agent badge styling for sender/target agent. |
| pkg/tui/components/tool/handoff/handoff.go | Switches to dynamic agent badge styling for sender/target agent. |
| pkg/tui/components/sidebar/sidebar.go | Switches current-agent prefix and name rendering to dynamic accent styling per agent. |
| pkg/tui/components/message/message.go | Switches message sender prefix to dynamic agent badge styling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // AgentBadgeColorsFor returns the badge foreground/background colors for a given agent name. | ||
| func AgentBadgeColorsFor(agentName string) AgentBadgeColors { | ||
| idx := agentIndex(agentName) | ||
| bgHex := agentColorPalette[idx] | ||
|
|
||
| theme := CurrentTheme() | ||
| fgHex := bestForegroundHex( | ||
| bgHex, | ||
| theme.Colors.TextBright, | ||
| theme.Colors.Background, | ||
| "#000000", | ||
| "#ffffff", | ||
| ) | ||
|
|
||
| return AgentBadgeColors{ | ||
| Fg: lipgloss.Color(fgHex), | ||
| Bg: lipgloss.Color(bgHex), | ||
| } |
There was a problem hiding this comment.
AgentBadgeColorsFor recomputes contrast (hex parsing + luminance math) on every call. Since badge rendering can happen frequently (e.g., per message render), consider memoizing the resolved foreground per (theme ref, palette index/bgHex) or precomputing on theme changes to avoid repeated parsing and math in hot render paths.
There was a problem hiding this comment.
precomputed all badge/accent styles in rebuildAgentColorCache(), called from
SetAgentOrder and InvalidateAgentColorCache (hooked into ApplyTheme). The hot render path now reads from cached slices.
cebba6a to
69d26b4
Compare
not yet configurable, it's what I wanted to discuss with you. |
69d26b4 to
da34a5c
Compare
|
Not sure at all cc @rumpl @krissetto @dgageot : Option 1: Add agent_colors section to ThemeColorsAdd agent_badge_colors and agent_accent_colors as string arrays to ThemeColors. Theme authors can override the full palettes. The merge logic would be: if the override provides In theme YAMLPros: Full control per theme, follows existing pattern, theme authors can curate exactly. Cons: Verbose (16×2 = 32 hex values to override), most theme authors won't bother, the Option 2: Derive agent colors from a single agent_base_hues listTheme provides a list of hue values (0–360) or a few seed colors. The system generates the full palette by adjusting saturation/lightness based on the theme's background Pros: Compact, auto-adapts to dark/light backgrounds, fewer values to maintain. Cons: More complex implementation, less precise control, HSL→hex conversion needed. Option 3: Theme provides a color_scale and system generates from itUse the theme's existing status/accent colors as seeds — brand, success, error, warning, info, accent, highlight, badge_accent, badge_info, badge_success — and derive the rest
Pros: Zero config for theme authors — it "just works" with any theme. No new YAML fields. Cons: Less control, the generated fills may not be aesthetically ideal, hard to predict Option 4: Hybrid — auto-derive with optional overrideCombine Option 3 (auto-derive from existing theme colors) as the default, with Option 1 (explicit array) as an optional override. If the theme YAML provides agent_badge_colors, Pros: Works out of the box for all existing themes, theme authors who care can override. Cons: Two code paths to maintain, but the override path is trivial. |
|
copilot feedbacks were addressed |
|
My preference would be Option 2, WDYT @krissetto ? |
|
@rumpl I also prefer Option 2. Explicit theme values could be nice to have if someone wants to define them, but generating the hues is better IMHO since there could be n agents in a team. Also @aheritier there is some code in tabbar/tab.go that does dynamic color selection for some bits of the UI that would've been too tedious to theme manually, we could adjust and consolidate the approach into something reusable across multiple places in the TUI as well so we end up with a consistent look and feel |
da34a5c to
bd0fa58
Compare
|
@rumpl @krissetto @dgageot please review with caution, it's bigger than I expected |
|
let's have the cagent bot take a pass at this :) /review |
There was a problem hiding this comment.
Review Summary
Found 1 high-severity race condition in the agent color registry that could cause incorrect color assignments when agents are dynamically reordered.
The issue affects all three style lookup functions and could result in agents displaying the wrong colors or fallback colors during concurrent access.
Summary
- High severity: 1 (race condition in style lookups)
- Medium severity: 0
- Low severity: 0
The rest of the implementation looks solid - the color math, palette generation, and synchronization primitives are all correctly implemented.
pkg/tui/styles/agent_colors.go
Outdated
|
|
||
| // AgentBadgeColorsFor returns the badge foreground/background colors for a given agent name. | ||
| func AgentBadgeColorsFor(agentName string) AgentBadgeColors { | ||
| idx := agentIndex(agentName) |
There was a problem hiding this comment.
🔴 HIGH SEVERITY: Race condition between agentIndex() call and style lookup
The function calls agentIndex() which releases its lock, then acquires a new RLock. Between these operations, SetAgentOrder() can rebuild the badgeStyles array, causing the returned idx to become stale.
Impact: While bounds checks prevent crashes, this causes incorrect color assignment:
- If the new palette is smaller,
idxmay exceed the new array length, causing fallback to default blue instead of the agent's assigned color - If the new palette is larger,
idxmay point to a different agent's colors in the new palette
Same issue exists in:
AgentBadgeStyleFor(line 143)AgentAccentStyleFor(line 160)
Fix: Change all three functions to hold the read lock across both the index lookup and array access:
func AgentBadgeColorsFor(agentName string) AgentBadgeColors {
agentRegistry.RLock()
defer agentRegistry.RUnlock()
idx, ok := agentRegistry.indices[agentName]
if !ok {
return AgentBadgeColors{
Fg: lipgloss.Color("#ffffff"),
Bg: lipgloss.Color("#1D63ED"),
}
}
size := len(agentRegistry.badgeStyles)
if size > 0 && idx%size < len(agentRegistry.badgeStyles) {
return agentRegistry.badgeStyles[idx%size].colors
}
return AgentBadgeColors{
Fg: lipgloss.Color("#ffffff"),
Bg: lipgloss.Color("#1D63ED"),
}
}Apply the same pattern to the other two functions.
Implement a flexible agent color palette with deterministic color assignment based on agent order. Introduces new functions to generate badge and accent styles for agents dynamically. Changes include: - Create agent color palettes for badges and accents - Add agent order tracking mechanism - Implement color selection based on agent index - Update components to use new dynamic styling functions
…til.go Extract color math functions from tabbar/tab.go and theme.go into a shared colorutil.go module. Adds HSL conversion, CIELAB perceptual distance, and hue-based palette generation functions. This eliminates duplicate luminance/contrast implementations and provides the foundation for theme-integrated agent color generation. Assisted-By: cagent
…ation Replace hardcoded agent color palettes with dynamic generation from HSL hue values. Each theme can define agent_hues (16 hue values 0-360) in its YAML; colors auto-adapt saturation and lightness based on the theme background (dark vs light). Validation across all 10 built-in themes: - WCAG AA badge contrast (≥4.5:1 fg/bg) — all pass - WCAG AA accent contrast (≥3.0:1 vs background) — all pass - CIE76 pairwise distinctness (ΔE≥10) — all pass - Color audit report available via go test -v Assisted-By: cagent
bd0fa58 to
c2ca1d5
Compare
Hold a single RLock across both the index lookup and the cached style array access in AgentBadgeColorsFor, AgentBadgeStyleFor, and AgentAccentStyleFor. Previously, agentIndex() acquired and released a separate lock before the caller re-locked to read the style arrays, allowing SetAgentOrder() to rebuild the registry in between and causing stale indices to reference wrong colors or fall through to defaults. Assisted-By: cagent
|
/review is it better ? cc @rumpl @krissetto @dgageot |
|
/review ☝️ this will work for you once you're officially part of the org :) |
There was a problem hiding this comment.
Review Summary
✅ No issues found
This PR implements a well-designed dynamic agent color styling system with excellent code quality:
- Thread-safe implementation: Proper use of
sync.RWMutexwith RLock for reads and Lock for writes - Safe initialization: Zero-initialized global struct with proper guards against empty arrays
- Robust color math: WCAG-compliant contrast ratios with guaranteed fallbacks to pure black/white
- Comprehensive testing: Full test coverage including WCAG AA contrast validation and CIE76 distinctness checks
- Lock-free atomic operations:
CurrentTheme()usesatomic.Pointer.Load()to avoid race conditions - Defensive programming: All array access protected by
size > 0checks before modulo operations
The code follows Go best practices and handles edge cases appropriately. The extensive test suite validates contrast ratios and color distinctness across all 10 built-in themes.
Reduce the public API surface of the styles package by unexporting color utility functions (parseHexRGB, contrastRatio, hslToRGB, etc.) and palette generators that are only used within the package. Consolidate repeated fallback badge/accent colors and styles into package-level variables and extract a lookupAgentIndex helper to remove duplicated lookup logic in AgentBadgeColorsFor, AgentBadgeStyleFor, and AgentAccentStyleFor. Assisted-By: cagent
Closes #1756
Summary
Assigns each agent in a multi-agent team a unique, deterministic color so users can visually distinguish agents across the entire TUI — message badges, sidebar entries, tab bar indicators, and handoff/transfer-task displays.
Colors are generated dynamically from hue values (0–360) defined per theme, with saturation and lightness auto-adapted based on the theme's background (dark → brighter, light → darker). This follows Option 2 from the design discussion, as agreed with @rumpl and @krissetto.
What changed
New: Color utility library (
pkg/tui/styles/colorutil.go)Shared color math extracted from
tabbar/tab.goandtheme.go, plus new functionality:ContrastRatio,EnsureContrast,MutedContrastFg)GenerateBadgePalette,GenerateAccentPalette)New: Agent color registry (
pkg/tui/styles/agent_colors.go)Thread-safe, cached mapping from agent name → palette index:
SetAgentOrder(names)— called when team info updatesAgentBadgeStyleFor(name)/AgentAccentStyleFor(name)— return precomputed lipgloss stylesInvalidateAgentColorCache()— called on theme change to regenerate stylesTheme integration
agent_huesfield inThemeColors(list of hue values 0–360)TUI component updates
AgentBadgeStyleFor(sender))AgentAccentStyleFor(agent.Name))MutedContrastFg,EnsureContrast) consolidated into sharedcolorutil.goValidation
All 10 built-in themes pass automated checks:
go test -v ./pkg/tui/styles/...Screenshots
Dark themes
Light themes
Files changed
pkg/tui/styles/colorutil.gopkg/tui/styles/colorutil_test.gopkg/tui/styles/agent_colors.gopkg/tui/styles/agent_colors_test.gopkg/tui/styles/theme.goAgentHuesfield, remove duplicated color helpers, hook cache invalidationpkg/tui/styles/theme_test.gomergeColorstest for newAgentHuesslice fieldpkg/tui/styles/themes/default.yamlagent_hueswith 16 default valuespkg/tui/service/sessionstate.goSetAgentOrderwhen available agents changepkg/tui/components/message/message.goAgentBadgeStyleFor(sender)pkg/tui/components/sidebar/sidebar.goAgentAccentStyleFor(agent.Name)pkg/tui/components/tabbar/tab.gocolorutil.go)pkg/tui/components/tabbar/tabbar.goMutedContrastFg/EnsureContrastpkg/tui/components/tool/handoff/handoff.gopkg/tui/components/tool/transfertask/transfertask.go