Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/tui/components/message/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func (mv *messageModel) senderPrefix(sender string) string {
if sender == "" {
return ""
}
return styles.AgentBadgeStyle.MarginLeft(2).Render(sender) + "\n\n"
return styles.AgentBadgeStyleFor(sender).MarginLeft(2).Render(sender) + "\n\n"
}

// sameAgentAsPrevious returns true if the previous message was from the same agent
Expand Down
8 changes: 4 additions & 4 deletions pkg/tui/components/sidebar/sidebar.go
Original file line number Diff line number Diff line change
Expand Up @@ -1091,17 +1091,17 @@ func (m *model) agentInfo(contentWidth int) string {
}

func (m *model) renderAgentEntry(content *strings.Builder, agent runtime.AgentDetails, isCurrent bool, index, contentWidth int) {
agentStyle := styles.AgentAccentStyleFor(agent.Name)
var prefix string
if isCurrent {
if m.workingAgent == agent.Name {
// Style the spinner with the same green as the agent name
prefix = styles.TabAccentStyle.Render(m.spinner.View()) + " "
prefix = agentStyle.Render(m.spinner.View()) + " "
} else {
prefix = styles.TabAccentStyle.Render("▶") + " "
prefix = agentStyle.Render("▶") + " "
}
}
// Agent name
agentNameText := prefix + styles.TabAccentStyle.Render(agent.Name)
agentNameText := prefix + agentStyle.Render(agent.Name)
// Shortcut hint (^1, ^2, etc.) - show for agents 1-9
var shortcutHint string
if index >= 0 && index < 9 {
Expand Down
125 changes: 6 additions & 119 deletions pkg/tui/components/tabbar/tab.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package tabbar

import (
"fmt"
"image/color"
"math"

"charm.land/lipgloss/v2"

Expand All @@ -26,17 +24,6 @@ const (
// attentionIndicator is shown before the title when the tab needs attention,
// replacing the running indicator to signal that user action is required.
attentionIndicator = "! "
// mutedContrastStrength controls how much the muted foreground shifts
// away from the background (0.0 = invisible, 1.0 = full black/white).
mutedContrastStrength = 0.45
// minIndicatorContrast is the minimum WCAG contrast ratio required for
// semantic indicator colors (running dot, attention bang). If the themed
// color doesn't meet this threshold against the tab background, it is
// automatically boosted while preserving its hue.
minIndicatorContrast = 4.5
// maxBoostSteps limits the blend iterations in ensureContrast to avoid
// infinite loops on degenerate inputs.
maxBoostSteps = 20

// dragSourceColorBoost controls how much the drag source tab is blended toward
// the active tab colors when it is not the active tab.
Expand Down Expand Up @@ -67,106 +54,6 @@ func (t Tab) Width() int { return t.width }
// and the close-button click area begins.
func (t Tab) MainZoneEnd() int { return t.mainZoneEnd }

// --- Color helpers ---

// sRGBLuminance returns the relative luminance of an sRGB color using the
// WCAG 2.x formula (linearized channel values, ITU-R BT.709 coefficients).
func sRGBLuminance(r, g, b float64) float64 {
linearize := func(c float64) float64 {
if c <= 0.03928 {
return c / 12.92
}
return math.Pow((c+0.055)/1.055, 2.4)
}
return 0.2126*linearize(r) + 0.7152*linearize(g) + 0.0722*linearize(b)
}

// colorToLinear extracts normalized [0,1] sRGB components from a color.Color.
func colorToLinear(c color.Color) (float64, float64, float64) {
r, g, b, _ := c.RGBA()
return float64(r) / 65535, float64(g) / 65535, float64(b) / 65535
}

// contrastRatio returns the WCAG 2.x contrast ratio between two colors.
func contrastRatio(fg, bg color.Color) float64 {
r1, g1, b1 := colorToLinear(fg)
r2, g2, b2 := colorToLinear(bg)
l1 := sRGBLuminance(r1, g1, b1)
l2 := sRGBLuminance(r2, g2, b2)
lighter := max(l1, l2)
darker := min(l1, l2)
return (lighter + 0.05) / (darker + 0.05)
}

// toHexColor formats normalized [0,1] RGB components as a lipgloss color.
func toHexColor(r, g, b float64) color.Color {
clamp := func(v float64) int {
if v < 0 {
return 0
}
if v > 1 {
return 255
}
return int(v * 255)
}
return lipgloss.Color(fmt.Sprintf("#%02x%02x%02x", clamp(r), clamp(g), clamp(b)))
}

// mutedContrastFg returns a foreground color that is visible but subtle against
// the given background. It blends the background toward white (for dark bg) or
// black (for light bg) by mutedContrastStrength.
func mutedContrastFg(bg color.Color) color.Color {
rf, gf, bf := colorToLinear(bg)

// Perceived luminance for direction decision (BT.601 for perceptual balance).
lum := 0.299*rf + 0.587*gf + 0.114*bf

var tgt float64
if lum > 0.5 {
tgt = 0.0
} else {
tgt = 1.0
}

s := mutedContrastStrength
return toHexColor(rf+(tgt-rf)*s, gf+(tgt-gf)*s, bf+(tgt-bf)*s)
}

// ensureContrast returns fg unchanged if it already meets minIndicatorContrast
// against bg. Otherwise it progressively blends fg toward white (on dark bg) or
// black (on light bg) until the threshold is met, preserving the original hue.
func ensureContrast(fg, bg color.Color) color.Color {
if contrastRatio(fg, bg) >= minIndicatorContrast {
return fg
}

rf, gf, bf := colorToLinear(fg)
bgR, bgG, bgB := colorToLinear(bg)
bgLum := sRGBLuminance(bgR, bgG, bgB)

// Blend toward white on dark backgrounds, toward black on light ones.
var tR, tG, tB float64
if bgLum > 0.5 {
tR, tG, tB = 0, 0, 0
} else {
tR, tG, tB = 1, 1, 1
}

for step := 1; step <= maxBoostSteps; step++ {
t := float64(step) / float64(maxBoostSteps)
nr := rf + (tR-rf)*t
ng := gf + (tG-gf)*t
nb := bf + (tB-bf)*t
candidate := toHexColor(nr, ng, nb)
if contrastRatio(candidate, bg) >= minIndicatorContrast {
return candidate
}
}

// Fallback: full contrast direction (should always meet the threshold).
return toHexColor(tR, tG, tB)
}

// dragRole describes a tab's role during a drag-and-drop operation.
type dragRole int

Expand All @@ -178,9 +65,9 @@ const (

// blendColors mixes two colors by the given ratio (0 = a, 1 = b).
func blendColors(a, b color.Color, ratio float64) color.Color {
ar, ag, ab := colorToLinear(a)
br, bg, bb := colorToLinear(b)
return toHexColor(
ar, ag, ab := styles.ColorToRGB(a)
br, bg, bb := styles.ColorToRGB(b)
return styles.RGBToColor(
ar+(br-ar)*ratio,
ag+(bg-ag)*ratio,
ab+(bb-ab)*ratio,
Expand Down Expand Up @@ -225,7 +112,7 @@ func renderTab(info messages.TabInfo, maxTitleLen, animFrame int, role dragRole)
}

// Close button color derived from this tab's background.
closeFg := mutedContrastFg(bgColor)
closeFg := styles.MutedContrastFg(bgColor)

// Fade all foreground elements when this tab is a bystander during drag.
if role == dragRoleBystander {
Expand All @@ -250,13 +137,13 @@ func renderTab(info messages.TabInfo, maxTitleLen, animFrame int, role dragRole)
case info.NeedsAttention:
// Attention takes priority over running: replace the streaming dot
// with a warning-colored indicator so it's obvious the tab needs action.
attnFg := ensureContrast(styles.Warning, bgColor)
attnFg := styles.EnsureContrast(styles.Warning, bgColor)
if role == dragRoleBystander {
attnFg = blendColors(attnFg, bgColor, dragBystanderDimAmount)
}
content += lipgloss.NewStyle().Foreground(attnFg).Background(bgColor).Bold(true).Render(attentionIndicator)
case info.IsRunning && !info.IsActive:
runFg := ensureContrast(styles.TabAccentFg, bgColor)
runFg := styles.EnsureContrast(styles.TabAccentFg, bgColor)
if role == dragRoleBystander {
runFg = blendColors(runFg, bgColor, dragBystanderDimAmount)
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/tui/components/tabbar/tabbar.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,11 +408,11 @@ func (t *TabBar) View() string {
}

// Compute "+" and arrow colors dynamically from the terminal background.
chromeFg := mutedContrastFg(styles.Background)
chromeFg := styles.MutedContrastFg(styles.Background)
plusStyle := lipgloss.NewStyle().Foreground(chromeFg)
arrowStyle := lipgloss.NewStyle().Foreground(chromeFg)
// Attention arrow style: warning-colored and bold so off-screen attention tabs are obvious.
attnArrowStyle := lipgloss.NewStyle().Foreground(ensureContrast(styles.Warning, styles.Background)).Bold(true)
attnArrowStyle := lipgloss.NewStyle().Foreground(styles.EnsureContrast(styles.Warning, styles.Background)).Bold(true)

var line string
var cursor int
Expand All @@ -436,7 +436,7 @@ func (t *TabBar) View() string {
var dropLine string
visualDrop := noTab
if t.drag.active && !t.drag.isNoOp() {
dropFg := ensureContrast(styles.TabAccentFg, styles.Background)
dropFg := styles.EnsureContrast(styles.TabAccentFg, styles.Background)
dropLine = lipgloss.NewStyle().Foreground(dropFg).Render(dropIndicator)
visualDrop = t.drag.dropIdx
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/tui/components/tool/handoff/handoff.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ func render(msg *types.Message, _ spinner.Spinner, _ service.SessionStateReader,
return ""
}

return styles.AgentBadgeStyle.MarginLeft(2).Render(msg.Sender) + " ─► " + styles.AgentBadgeStyle.Render(params.Agent)
return styles.AgentBadgeStyleFor(msg.Sender).MarginLeft(2).Render(msg.Sender) + " ─► " + styles.AgentBadgeStyleFor(params.Agent).Render(params.Agent)
}
4 changes: 2 additions & 2 deletions pkg/tui/components/tool/transfertask/transfertask.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ func render(msg *types.Message, _ spinner.Spinner, _ service.SessionStateReader,
return ""
}

header := styles.AgentBadgeStyle.MarginLeft(2).Render(msg.Sender) +
header := styles.AgentBadgeStyleFor(msg.Sender).MarginLeft(2).Render(msg.Sender) +
" calls " +
styles.AgentBadgeStyle.Render(params.Agent)
styles.AgentBadgeStyleFor(params.Agent).Render(params.Agent)

// Calculate the icon with its margin
icon := styles.ToolCompletedIcon.Render("✓")
Expand Down
7 changes: 7 additions & 0 deletions pkg/tui/service/sessionstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package service
import (
"github.com/docker/cagent/pkg/runtime"
"github.com/docker/cagent/pkg/session"
"github.com/docker/cagent/pkg/tui/styles"
"github.com/docker/cagent/pkg/tui/types"
"github.com/docker/cagent/pkg/userconfig"
)
Expand Down Expand Up @@ -116,6 +117,12 @@ func (s *SessionState) AvailableAgents() []runtime.AgentDetails {

func (s *SessionState) SetAvailableAgents(availableAgents []runtime.AgentDetails) {
s.availableAgents = availableAgents

names := make([]string, len(availableAgents))
for i, a := range availableAgents {
names[i] = a.Name
}
styles.SetAgentOrder(names)
}

func (s *SessionState) GetCurrentAgent() runtime.AgentDetails {
Expand Down
Loading