chore: change LSP/MCP loading icon color + refactor icons + theme prep

Christian Rocha created

Change summary

internal/tui/components/chat/messages/messages.go |  1 
internal/tui/components/core/core.go              | 39 ++++++----------
internal/tui/components/files/files.go            |  2 
internal/tui/components/lsp/lsp.go                | 12 ++--
internal/tui/components/mcp/mcp.go                | 12 ++---
internal/tui/styles/crush.go                      | 11 ++++
internal/tui/styles/theme.go                      |  6 ++
7 files changed, 41 insertions(+), 42 deletions(-)

Detailed changes

internal/tui/components/chat/messages/messages.go 🔗

@@ -278,7 +278,6 @@ func (m *messageCmp) renderThinkingContent() string {
 			opts := core.StatusOpts{
 				Title:       "Thought for",
 				Description: duration.String(),
-				NoIcon:      true,
 			}
 			return t.S().Base.PaddingLeft(1).Render(core.Status(opts, m.textWidth()-1))
 		} else if finishReason != nil && finishReason.Reason == message.FinishReasonCanceled {

internal/tui/components/core/core.go 🔗

@@ -82,41 +82,30 @@ func Title(title string, width int) string {
 }
 
 type StatusOpts struct {
-	Icon             string
-	IconColor        color.Color
-	NoIcon           bool // If true, no icon will be displayed
+	Icon             string // if empty no icon will be shown
 	Title            string
 	TitleColor       color.Color
 	Description      string
 	DescriptionColor color.Color
-	ExtraContent     string // Additional content to append after the description
+	ExtraContent     string // additional content to append after the description
 }
 
-func Status(ops StatusOpts, width int) string {
+func Status(opts StatusOpts, width int) string {
 	t := styles.CurrentTheme()
-	icon := "●"
-	iconColor := t.Success
-	if ops.Icon != "" {
-		icon = ops.Icon
-	} else if ops.NoIcon {
-		icon = ""
-	}
-	if ops.IconColor != nil {
-		iconColor = ops.IconColor
-	}
-	title := ops.Title
+	icon := opts.Icon
+	title := opts.Title
 	titleColor := t.FgMuted
-	if ops.TitleColor != nil {
-		titleColor = ops.TitleColor
+	if opts.TitleColor != nil {
+		titleColor = opts.TitleColor
 	}
-	description := ops.Description
+	description := opts.Description
 	descriptionColor := t.FgSubtle
-	if ops.DescriptionColor != nil {
-		descriptionColor = ops.DescriptionColor
+	if opts.DescriptionColor != nil {
+		descriptionColor = opts.DescriptionColor
 	}
 	title = t.S().Base.Foreground(titleColor).Render(title)
 	if description != "" {
-		extraContentWidth := lipgloss.Width(ops.ExtraContent)
+		extraContentWidth := lipgloss.Width(opts.ExtraContent)
 		if extraContentWidth > 0 {
 			extraContentWidth += 1
 		}
@@ -126,11 +115,11 @@ func Status(ops StatusOpts, width int) string {
 
 	content := []string{}
 	if icon != "" {
-		content = append(content, t.S().Base.Foreground(iconColor).Render(icon))
+		content = append(content, icon)
 	}
 	content = append(content, title, description)
-	if ops.ExtraContent != "" {
-		content = append(content, ops.ExtraContent)
+	if opts.ExtraContent != "" {
+		content = append(content, opts.ExtraContent)
 	}
 
 	return strings.Join(content, " ")

internal/tui/components/files/files.go 🔗

@@ -98,8 +98,6 @@ func RenderFileList(fileSlice []SessionFile, opts RenderOptions) []string {
 		fileList = append(fileList,
 			core.Status(
 				core.StatusOpts{
-					IconColor:    t.FgMuted,
-					NoIcon:       true,
 					Title:        filePath,
 					ExtraContent: extraContent,
 				},

internal/tui/components/lsp/lsp.go 🔗

@@ -57,22 +57,22 @@ func RenderLSPList(lspClients map[string]*lsp.Client, opts RenderOptions) []stri
 		}
 
 		// Determine icon color and description based on state
-		iconColor := t.FgMuted
+		// iconColor := t.FgMuted
+		icon := t.ItemOfflineIcon
 		description := l.LSP.Command
 
 		if l.LSP.Disabled {
-			iconColor = t.FgMuted
 			description = t.S().Subtle.Render("disabled")
 		} else if state, exists := lspStates[l.Name]; exists {
 			switch state.State {
 			case lsp.StateStarting:
-				iconColor = t.Yellow
+				icon = t.ItemBusyIcon
 				description = t.S().Subtle.Render("starting...")
 			case lsp.StateReady:
-				iconColor = t.Success
+				icon = t.ItemOnlineIcon
 				description = l.LSP.Command
 			case lsp.StateError:
-				iconColor = t.Red
+				icon = t.ItemErrorIcon
 				if state.Error != nil {
 					description = t.S().Subtle.Render(fmt.Sprintf("error: %s", state.Error.Error()))
 				} else {
@@ -119,7 +119,7 @@ func RenderLSPList(lspClients map[string]*lsp.Client, opts RenderOptions) []stri
 		lspList = append(lspList,
 			core.Status(
 				core.StatusOpts{
-					IconColor:    iconColor,
+					Icon:         icon.String(),
 					Title:        l.Name,
 					Description:  description,
 					ExtraContent: extraContent,

internal/tui/components/mcp/mcp.go 🔗

@@ -54,25 +54,24 @@ func RenderMCPList(opts RenderOptions) []string {
 		}
 
 		// Determine icon and color based on state
-		iconColor := t.FgMuted
+		icon := t.ItemOfflineIcon
 		description := l.MCP.Command
 		extraContent := ""
 
 		if state, exists := mcpStates[l.Name]; exists {
 			switch state.State {
 			case agent.MCPStateDisabled:
-				iconColor = t.FgMuted
 				description = t.S().Subtle.Render("disabled")
 			case agent.MCPStateStarting:
-				iconColor = t.Yellow
+				icon = t.ItemBusyIcon
 				description = t.S().Subtle.Render("starting...")
 			case agent.MCPStateConnected:
-				iconColor = t.Success
+				icon = t.ItemOnlineIcon
 				if state.ToolCount > 0 {
 					extraContent = t.S().Subtle.Render(fmt.Sprintf("(%d tools)", state.ToolCount))
 				}
 			case agent.MCPStateError:
-				iconColor = t.Red
+				icon = t.ItemErrorIcon
 				if state.Error != nil {
 					description = t.S().Subtle.Render(fmt.Sprintf("error: %s", state.Error.Error()))
 				} else {
@@ -80,14 +79,13 @@ func RenderMCPList(opts RenderOptions) []string {
 				}
 			}
 		} else if l.MCP.Disabled {
-			iconColor = t.FgMuted
 			description = t.S().Subtle.Render("disabled")
 		}
 
 		mcpList = append(mcpList,
 			core.Status(
 				core.StatusOpts{
-					IconColor:    iconColor,
+					Icon:         icon.String(),
 					Title:        l.Name,
 					Description:  description,
 					ExtraContent: extraContent,

internal/tui/styles/crush.go 🔗

@@ -1,11 +1,12 @@
 package styles
 
 import (
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/exp/charmtone"
 )
 
 func NewCrushTheme() *Theme {
-	return &Theme{
+	t := &Theme{
 		Name:   "crush",
 		IsDark: true,
 
@@ -54,4 +55,12 @@ func NewCrushTheme() *Theme {
 		RedLight: charmtone.Salmon,
 		Cherry:   charmtone.Cherry,
 	}
+
+	// LSP and MCP status.
+	t.ItemOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Squid).SetString("●")
+	t.ItemBusyIcon = t.ItemOfflineIcon.Foreground(charmtone.Citron)
+	t.ItemErrorIcon = t.ItemOfflineIcon.Foreground(charmtone.Coral)
+	t.ItemOnlineIcon = t.ItemOfflineIcon.Foreground(charmtone.Guac)
+
+	return t
 }

internal/tui/styles/theme.go 🔗

@@ -74,6 +74,12 @@ type Theme struct {
 	RedLight color.Color
 	Cherry   color.Color
 
+	// LSP and MCP status indicators.
+	ItemOfflineIcon lipgloss.Style
+	ItemBusyIcon    lipgloss.Style
+	ItemErrorIcon   lipgloss.Style
+	ItemOnlineIcon  lipgloss.Style
+
 	styles *Styles
 }