on main help sign in
Merge pull request #571 from charmbracelet/status-icons

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

commit1504a925891d4da6970e69e98fe18f771f3fa79a authorKujtim Hoxha <kujtimii.h@gmail.com> dateTue 5 Aug 2025, 15:59 (1 year ago) parent989e56e40e16d1
23 files changed+56−59
CRUSH.md+2 internal/tui/components/chat/messages/messages.go−1 internal/tui/components/core/core.go+14−25 internal/tui/components/core/status_test.go−3 internal/tui/components/core/testdata/TestStatus/AllFieldsWithExtraContent.golden+1−1 internal/tui/components/core/testdata/TestStatus/Default.golden+1−1 internal/tui/components/core/testdata/TestStatus/EmptyDescription.golden+1−1 internal/tui/components/core/testdata/TestStatus/LongDescription.golden+1−1 internal/tui/components/core/testdata/TestStatus/NarrowWidth.golden+1−1 internal/tui/components/core/testdata/TestStatus/VeryNarrowWidth.golden+1−1 internal/tui/components/core/testdata/TestStatus/WithColors.golden+1−1 internal/tui/components/core/testdata/TestStatus/WithCustomIcon.golden+1−1 internal/tui/components/core/testdata/TestStatus/WithExtraContent.golden+1−1 internal/tui/components/core/testdata/TestStatusTruncation/Width20.golden+1−1 internal/tui/components/core/testdata/TestStatusTruncation/Width30.golden+1−1 internal/tui/components/core/testdata/TestStatusTruncation/Width40.golden+1−1 internal/tui/components/core/testdata/TestStatusTruncation/Width50.golden+1−1 internal/tui/components/core/testdata/TestStatusTruncation/Width60.golden+1−1 internal/tui/components/files/files.go−2 internal/tui/components/lsp/lsp.go+5−6 internal/tui/components/mcp/mcp.go+5−7 internal/tui/styles/crush.go+10−1 internal/tui/styles/theme.go+6
CRUSH.md+2
@@ -4,6 +4,8 @@
44
55- **Build**: `go build .` or `go run .`
66- **Test**: `task test` or `go test ./...` (run single test: `go test ./internal/llm/prompt -run TestGetContextFromPaths`)
7+- **Update Golden Files**: `go test ./... -update` (regenerates .golden files when test output changes)
8+ - Update specific package: `go test ./internal/tui/components/core -update` (in this case, we're updating "core")
79- **Lint**: `task lint-fix`
810- **Format**: `task fmt` (gofumpt -w .)
911- **Dev**: `task dev` (runs with profiling enabled)
internal/tui/components/chat/messages/messages.go−1
@@ -281,7 +281,6 @@ func (m *messageCmp) renderThinkingContent() string {
281281 opts := core.StatusOpts{
282282 Title: "Thought for",
283283 Description: duration.String(),
284− NoIcon: true,
285284 }
286285 return t.S().Base.PaddingLeft(1).Render(core.Status(opts, m.textWidth()-1))
287286 } else if finishReason != nil && finishReason.Reason == message.FinishReasonCanceled {
internal/tui/components/core/core.go+14−25
@@ -82,41 +82,30 @@ func Title(title string, width int) string {
8282}
8383
8484type StatusOpts struct {
85− Icon string
86− IconColor color.Color
87− NoIcon bool // If true, no icon will be displayed
85+ Icon string // if empty no icon will be shown
8886 Title string
8987 TitleColor color.Color
9088 Description string
9189 DescriptionColor color.Color
92− ExtraContent string // Additional content to append after the description
90+ ExtraContent string // additional content to append after the description
9391}
9492
95−func Status(ops StatusOpts, width int) string {
93+func Status(opts StatusOpts, width int) string {
9694 t := styles.CurrentTheme()
97− icon := "●"
98− iconColor := t.Success
99− if ops.Icon != "" {
100− icon = ops.Icon
101− } else if ops.NoIcon {
102− icon = ""
103− }
104− if ops.IconColor != nil {
105− iconColor = ops.IconColor
106− }
107− title := ops.Title
95+ icon := opts.Icon
96+ title := opts.Title
10897 titleColor := t.FgMuted
109− if ops.TitleColor != nil {
110− titleColor = ops.TitleColor
98+ if opts.TitleColor != nil {
99+ titleColor = opts.TitleColor
111100 }
112− description := ops.Description
101+ description := opts.Description
113102 descriptionColor := t.FgSubtle
114− if ops.DescriptionColor != nil {
115− descriptionColor = ops.DescriptionColor
103+ if opts.DescriptionColor != nil {
104+ descriptionColor = opts.DescriptionColor
116105 }
117106 title = t.S().Base.Foreground(titleColor).Render(title)
118107 if description != "" {
119− extraContentWidth := lipgloss.Width(ops.ExtraContent)
108+ extraContentWidth := lipgloss.Width(opts.ExtraContent)
120109 if extraContentWidth > 0 {
121110 extraContentWidth += 1
122111 }
@@ -126,11 +115,11 @@ func Status(ops StatusOpts, width int) string {
126115
127116 content := []string{}
128117 if icon != "" {
129− content = append(content, t.S().Base.Foreground(iconColor).Render(icon))
118+ content = append(content, icon)
130119 }
131120 content = append(content, title, description)
132− if ops.ExtraContent != "" {
133− content = append(content, ops.ExtraContent)
121+ if opts.ExtraContent != "" {
122+ content = append(content, opts.ExtraContent)
134123 }
135124
136125 return strings.Join(content, " ")
internal/tui/components/core/status_test.go−3
@@ -37,7 +37,6 @@ func TestStatus(t *testing.T) {
3737 {
3838 name: "NoIcon",
3939 opts: core.StatusOpts{
40− NoIcon: true,
4140 Title: "Info",
4241 Description: "This status has no icon",
4342 },
@@ -47,7 +46,6 @@ func TestStatus(t *testing.T) {
4746 name: "WithColors",
4847 opts: core.StatusOpts{
4948 Icon: "⚠",
50− IconColor: color.RGBA{255, 165, 0, 255}, // Orange
5149 Title: "Warning",
5250 TitleColor: color.RGBA{255, 255, 0, 255}, // Yellow
5351 Description: "This is a warning message",
@@ -102,7 +100,6 @@ func TestStatus(t *testing.T) {
102100 name: "AllFieldsWithExtraContent",
103101 opts: core.StatusOpts{
104102 Icon: "🚀",
105− IconColor: color.RGBA{0, 255, 0, 255}, // Green
106103 Title: "Deployment",
107104 TitleColor: color.RGBA{0, 0, 255, 255}, // Blue
108105 Description: "Deploying to production environment",
internal/tui/components/core/testdata/TestStatus/AllFieldsWithExtraContent.golden+1−1
@@ -1 +1 @@
1−🚀 Deployment Deploying to production environment v1.2.3
1+🚀 Deployment Deploying to production environment v1.2.3
internal/tui/components/core/testdata/TestStatus/Default.golden+1−1
@@ -1 +1 @@
1−● Status Everything is working fine
1+Status Everything is working fine
internal/tui/components/core/testdata/TestStatus/EmptyDescription.golden+1−1
@@ -1 +1 @@
1−● Title Only 
1+● Title Only 
internal/tui/components/core/testdata/TestStatus/LongDescription.golden+1−1
@@ -1 +1 @@
1−● Processing This is a very long description that should be…
1+Processing This is a very long description that should be …
internal/tui/components/core/testdata/TestStatus/NarrowWidth.golden+1−1
@@ -1 +1 @@
1−● Status Short message
1+● Status Short message
internal/tui/components/core/testdata/TestStatus/VeryNarrowWidth.golden+1−1
@@ -1 +1 @@
1−● Test This will be…
1+● Test This will be…
internal/tui/components/core/testdata/TestStatus/WithColors.golden+1−1
@@ -1 +1 @@
1−⚠ Warning This is a warning message
1+⚠ Warning This is a warning message
internal/tui/components/core/testdata/TestStatus/WithCustomIcon.golden+1−1
@@ -1 +1 @@
1−✓ Success Operation completed successfully
1+✓ Success Operation completed successfully
internal/tui/components/core/testdata/TestStatus/WithExtraContent.golden+1−1
@@ -1 +1 @@
1−● Build Building project [2/5]
1+Build Building project [2/5]
internal/tui/components/core/testdata/TestStatusTruncation/Width20.golden+1−1
@@ -1 +1 @@
1−● Very Long Title  [extra]
1+● Very Long Title  [extra]
internal/tui/components/core/testdata/TestStatusTruncation/Width30.golden+1−1
@@ -1 +1 @@
1−● Very Long Title Thi… [extra]
1+● Very Long Title Thi… [extra]
internal/tui/components/core/testdata/TestStatusTruncation/Width40.golden+1−1
@@ -1 +1 @@
1−● Very Long Title This is an ex… [extra]
1+● Very Long Title This is an ex… [extra]
internal/tui/components/core/testdata/TestStatusTruncation/Width50.golden+1−1
@@ -1 +1 @@
1−● Very Long Title This is an extremely lo… [extra]
1+● Very Long Title This is an extremely lo… [extra]
internal/tui/components/core/testdata/TestStatusTruncation/Width60.golden+1−1
@@ -1 +1 @@
1−● Very Long Title This is an extremely long descrip… [extra]
1+● Very Long Title This is an extremely long descrip… [extra]
internal/tui/components/files/files.go−2
@@ -98,8 +98,6 @@ func RenderFileList(fileSlice []SessionFile, opts RenderOptions) []string {
9898 fileList = append(fileList,
9999 core.Status(
100100 core.StatusOpts{
101− IconColor: t.FgMuted,
102− NoIcon: true,
103101 Title: filePath,
104102 ExtraContent: extraContent,
105103 },
internal/tui/components/lsp/lsp.go+5−6
@@ -57,22 +57,21 @@ func RenderLSPList(lspClients map[string]*lsp.Client, opts RenderOptions) []stri
5757 }
5858
5959 // Determine icon color and description based on state
60− iconColor := t.FgMuted
60+ icon := t.ItemOfflineIcon
6161 description := l.LSP.Command
6262
6363 if l.LSP.Disabled {
64− iconColor = t.FgMuted
6564 description = t.S().Subtle.Render("disabled")
6665 } else if state, exists := lspStates[l.Name]; exists {
6766 switch state.State {
6867 case lsp.StateStarting:
69− iconColor = t.Yellow
68+ icon = t.ItemBusyIcon
7069 description = t.S().Subtle.Render("starting...")
7170 case lsp.StateReady:
72− iconColor = t.Success
71+ icon = t.ItemOnlineIcon
7372 description = l.LSP.Command
7473 case lsp.StateError:
75− iconColor = t.Red
74+ icon = t.ItemErrorIcon
7675 if state.Error != nil {
7776 description = t.S().Subtle.Render(fmt.Sprintf("error: %s", state.Error.Error()))
7877 } else {
@@ -119,7 +118,7 @@ func RenderLSPList(lspClients map[string]*lsp.Client, opts RenderOptions) []stri
119118 lspList = append(lspList,
120119 core.Status(
121120 core.StatusOpts{
122− IconColor: iconColor,
121+ Icon: icon.String(),
123122 Title: l.Name,
124123 Description: description,
125124 ExtraContent: extraContent,
internal/tui/components/mcp/mcp.go+5−7
@@ -54,25 +54,24 @@ func RenderMCPList(opts RenderOptions) []string {
5454 }
5555
5656 // Determine icon and color based on state
57− iconColor := t.FgMuted
57+ icon := t.ItemOfflineIcon
5858 description := l.MCP.Command
5959 extraContent := ""
6060
6161 if state, exists := mcpStates[l.Name]; exists {
6262 switch state.State {
6363 case agent.MCPStateDisabled:
64− iconColor = t.FgMuted
6564 description = t.S().Subtle.Render("disabled")
6665 case agent.MCPStateStarting:
67− iconColor = t.Yellow
66+ icon = t.ItemBusyIcon
6867 description = t.S().Subtle.Render("starting...")
6968 case agent.MCPStateConnected:
70− iconColor = t.Success
69+ icon = t.ItemOnlineIcon
7170 if state.ToolCount > 0 {
7271 extraContent = t.S().Subtle.Render(fmt.Sprintf("(%d tools)", state.ToolCount))
7372 }
7473 case agent.MCPStateError:
75− iconColor = t.Red
74+ icon = t.ItemErrorIcon
7675 if state.Error != nil {
7776 description = t.S().Subtle.Render(fmt.Sprintf("error: %s", state.Error.Error()))
7877 } else {
@@ -80,14 +79,13 @@ func RenderMCPList(opts RenderOptions) []string {
8079 }
8180 }
8281 } else if l.MCP.Disabled {
83− iconColor = t.FgMuted
8482 description = t.S().Subtle.Render("disabled")
8583 }
8684
8785 mcpList = append(mcpList,
8886 core.Status(
8987 core.StatusOpts{
90− IconColor: iconColor,
88+ Icon: icon.String(),
9189 Title: l.Name,
9290 Description: description,
9391 ExtraContent: extraContent,
internal/tui/styles/crush.go+10−1
@@ -1,11 +1,12 @@
11package styles
22
33import (
4+ "github.com/charmbracelet/lipgloss/v2"
45 "github.com/charmbracelet/x/exp/charmtone"
56)
67
78func NewCrushTheme() *Theme {
8− return &Theme{
9+ t := &Theme{
910 Name: "crush",
1011 IsDark: true,
1112
@@ -54,4 +55,12 @@ func NewCrushTheme() *Theme {
5455 RedLight: charmtone.Salmon,
5556 Cherry: charmtone.Cherry,
5657 }
58+
59+ // LSP and MCP status.
60+ t.ItemOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Squid).SetString("●")
61+ t.ItemBusyIcon = t.ItemOfflineIcon.Foreground(charmtone.Citron)
62+ t.ItemErrorIcon = t.ItemOfflineIcon.Foreground(charmtone.Coral)
63+ t.ItemOnlineIcon = t.ItemOfflineIcon.Foreground(charmtone.Guac)
64+
65+ return t
5766}
internal/tui/styles/theme.go+6
@@ -74,6 +74,12 @@ type Theme struct {
7474 RedLight color.Color
7575 Cherry color.Color
7676
77+ // LSP and MCP status indicators.
78+ ItemOfflineIcon lipgloss.Style
79+ ItemBusyIcon lipgloss.Style
80+ ItemErrorIcon lipgloss.Style
81+ ItemOnlineIcon lipgloss.Style
82+
7783 styles *Styles
7884}
7985