on main help sign in
Merge pull request #2692 from charmbracelet/hypercrush

chore(styles): overhaul style definitions

commite5b5a40547817d0c5e75fd4112a89d01b7756932 authorChristian Rocha <christian@rocha.is> dateFri 24 Apr 2026, 09:23 (6 months ago) parent549f71781daa26
41 files changed+1094−767
go.mod+1−1 go.sum+2−2 internal/app/app.go+3−3 internal/cmd/run.go+3−3 internal/ui/chat/assistant.go+14−14 internal/ui/chat/docker_mcp.go+5−5 internal/ui/chat/messages.go+5−5 internal/ui/chat/todos.go+6−6 internal/ui/chat/tools.go+7−7 internal/ui/chat/user.go+2−2 internal/ui/common/button.go+2−2 internal/ui/common/elements.go+14−14 internal/ui/common/markdown.go+3−3 internal/ui/dialog/api_key_input.go+2−2 internal/ui/dialog/arguments.go+1−1 internal/ui/dialog/commands.go+2−2 internal/ui/dialog/commands_item.go+2−2 internal/ui/dialog/common.go+6−4 internal/ui/dialog/models.go+5−5 internal/ui/dialog/models_item.go+3−3 internal/ui/dialog/oauth.go+10−13 internal/ui/dialog/permissions.go+4−4 internal/ui/dialog/quit.go+2−2 internal/ui/dialog/reasoning.go+2−2 internal/ui/dialog/sessions_item.go+2−2 internal/ui/logo/example/main.go+49 internal/ui/logo/letterforms.go+419 internal/ui/logo/logo.go+51−236 internal/ui/logo/rand.go+2−2 internal/ui/model/header.go+2−2 internal/ui/model/layout_test.go+1−1 internal/ui/model/lsp.go+16−16 internal/ui/model/mcp.go+16−16 internal/ui/model/pills.go+6−6 internal/ui/model/session.go+3−3 internal/ui/model/sidebar.go+1−1 internal/ui/model/skills.go+8−8 internal/ui/model/skills_test.go+2−2 internal/ui/model/ui.go+14−14 internal/ui/styles/grad.go+9−56 internal/ui/styles/styles.go+387−295
go.mod+1−1
@@ -31,7 +31,7 @@ require (
3131 github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f
3232 github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
3333 github.com/charmbracelet/x/exp/ordered v0.1.0
34− github.com/charmbracelet/x/exp/slice v0.0.0-20260209194814-eeb2896ac759
34+ github.com/charmbracelet/x/exp/slice v0.0.0-20260422141420-a6cbdff8a7e2
3535 github.com/charmbracelet/x/exp/strings v0.1.0
3636 github.com/charmbracelet/x/powernap v0.1.4
3737 github.com/charmbracelet/x/term v0.2.2
go.sum+2−2
@@ -122,8 +122,8 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6g
122122github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
123123github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
124124github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
125−github.com/charmbracelet/x/exp/slice v0.0.0-20260209194814-eeb2896ac759 h1:96wFGlst+IDv3dIf5q29nw470wJYB3YAgemiciLZcG0=
126−github.com/charmbracelet/x/exp/slice v0.0.0-20260209194814-eeb2896ac759/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
125+github.com/charmbracelet/x/exp/slice v0.0.0-20260422141420-a6cbdff8a7e2 h1:EXQ7j9kQUGILoSIbxrr1osK9Ca3xqc6EJ7710FIiI3U=
126+github.com/charmbracelet/x/exp/slice v0.0.0-20260422141420-a6cbdff8a7e2/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
127127github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA=
128128github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8=
129129github.com/charmbracelet/x/json v0.2.0 h1:DqB+ZGx2h+Z+1s98HOuOyli+i97wsFQIxP2ZQANTPrQ=
internal/app/app.go+3−3
@@ -242,14 +242,14 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
242242 if f, ok := output.(*os.File); ok && stdinTTY && stdoutTTY {
243243 hasDarkBG = lipgloss.HasDarkBackground(os.Stdin, f)
244244 }
245− defaultFG := lipgloss.LightDark(hasDarkBG)(charmtone.Pepper, t.FgBase)
245+ defaultFG := lipgloss.LightDark(hasDarkBG)(charmtone.Pepper, t.WorkingLabelColor)
246246
247247 spinner = format.NewSpinner(ctx, cancel, anim.Settings{
248248 Size: 10,
249249 Label: "Generating",
250250 LabelColor: defaultFG,
251− GradColorA: t.Primary,
252− GradColorB: t.Secondary,
251+ GradColorA: t.WorkingGradFromColor,
252+ GradColorB: t.WorkingGradToColor,
253253 CycleColors: true,
254254 })
255255 spinner.Start()
internal/cmd/run.go+3−3
@@ -195,14 +195,14 @@ func runNonInteractive(
195195 if stdinTTY && stdoutTTY {
196196 hasDarkBG = lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
197197 }
198− defaultFG := lipgloss.LightDark(hasDarkBG)(charmtone.Pepper, t.FgBase)
198+ defaultFG := lipgloss.LightDark(hasDarkBG)(charmtone.Pepper, t.WorkingLabelColor)
199199
200200 spinner = format.NewSpinner(ctx, cancel, anim.Settings{
201201 Size: 10,
202202 Label: "Generating",
203203 LabelColor: defaultFG,
204− GradColorA: t.Primary,
205− GradColorB: t.Secondary,
204+ GradColorA: t.WorkingGradFromColor,
205+ GradColorB: t.WorkingGradToColor,
206206 CycleColors: true,
207207 })
208208 spinner.Start()
internal/ui/chat/assistant.go+14−14
@@ -48,9 +48,9 @@ func NewAssistantMessageItem(sty *styles.Styles, message *message.Message) Messa
4848 a.anim = anim.New(anim.Settings{
4949 ID: a.ID(),
5050 Size: 15,
51− GradColorA: sty.Primary,
52− GradColorB: sty.Secondary,
53− LabelColor: sty.FgBase,
51+ GradColorA: sty.WorkingGradFromColor,
52+ GradColorB: sty.WorkingGradToColor,
53+ LabelColor: sty.WorkingLabelColor,
5454 CycleColors: true,
5555 })
5656 return a
@@ -112,8 +112,8 @@ func (a *AssistantMessageItem) Render(width int) string {
112112 // it's wrapping logic.
113113 // We already know that the content is wrapped to the correct width in
114114 // RawRender, so we can just apply the styles directly to each line.
115− focused := a.sty.Chat.Message.AssistantFocused.Render()
116− blurred := a.sty.Chat.Message.AssistantBlurred.Render()
115+ focused := a.sty.Messages.AssistantFocused.Render()
116+ blurred := a.sty.Messages.AssistantBlurred.Render()
117117 rendered := a.RawRender(width)
118118 lines := strings.Split(rendered, "\n")
119119 for i, line := range lines {
@@ -149,7 +149,7 @@ func (a *AssistantMessageItem) renderMessageContent(width int) string {
149149 if a.message.IsFinished() {
150150 switch a.message.FinishReason() {
151151 case message.FinishReasonCanceled:
152− messageParts = append(messageParts, a.sty.Base.Italic(true).Render("Canceled"))
152+ messageParts = append(messageParts, a.sty.Messages.AssistantCanceled.Render("Canceled"))
153153 case message.FinishReasonError:
154154 messageParts = append(messageParts, a.renderError(width))
155155 }
@@ -160,7 +160,7 @@ func (a *AssistantMessageItem) renderMessageContent(width int) string {
160160
161161// renderThinking renders the thinking/reasoning content with footer.
162162func (a *AssistantMessageItem) renderThinking(thinking string, width int) string {
163− renderer := common.PlainMarkdownRenderer(a.sty, width)
163+ renderer := common.QuietMarkdownRenderer(a.sty, width)
164164 rendered, err := renderer.Render(thinking)
165165 if err != nil {
166166 rendered = thinking
@@ -173,13 +173,13 @@ func (a *AssistantMessageItem) renderThinking(thinking string, width int) string
173173 isTruncated := totalLines > maxCollapsedThinkingHeight
174174 if !a.thinkingExpanded && isTruncated {
175175 lines = lines[totalLines-maxCollapsedThinkingHeight:]
176− hint := a.sty.Chat.Message.ThinkingTruncationHint.Render(
176+ hint := a.sty.Messages.ThinkingTruncationHint.Render(
177177 fmt.Sprintf(assistantMessageTruncateFormat, totalLines-maxCollapsedThinkingHeight),
178178 )
179179 lines = append([]string{hint, ""}, lines...)
180180 }
181181
182− thinkingStyle := a.sty.Chat.Message.ThinkingBox.Width(width)
182+ thinkingStyle := a.sty.Messages.ThinkingBox.Width(width)
183183 result := thinkingStyle.Render(strings.Join(lines, "\n"))
184184 a.thinkingBoxHeight = lipgloss.Height(result)
185185
@@ -188,8 +188,8 @@ func (a *AssistantMessageItem) renderThinking(thinking string, width int) string
188188 if !a.message.IsThinking() || len(a.message.ToolCalls()) > 0 {
189189 duration := a.message.ThinkingDuration()
190190 if duration.String() != "0s" {
191− footer = a.sty.Chat.Message.ThinkingFooterTitle.Render("Thought for ") +
192− a.sty.Chat.Message.ThinkingFooterDuration.Render(duration.String())
191+ footer = a.sty.Messages.ThinkingFooterTitle.Render("Thought for ") +
192+ a.sty.Messages.ThinkingFooterDuration.Render(duration.String())
193193 }
194194 }
195195
@@ -222,10 +222,10 @@ func (a *AssistantMessageItem) renderSpinning() string {
222222// renderError renders an error message.
223223func (a *AssistantMessageItem) renderError(width int) string {
224224 finishPart := a.message.FinishPart()
225− errTag := a.sty.Chat.Message.ErrorTag.Render("ERROR")
225+ errTag := a.sty.Messages.ErrorTag.Render("ERROR")
226226 truncated := ansi.Truncate(finishPart.Message, width-2-lipgloss.Width(errTag), "...")
227− title := fmt.Sprintf("%s %s", errTag, a.sty.Chat.Message.ErrorTitle.Render(truncated))
228− details := a.sty.Chat.Message.ErrorDetails.Width(width - 2).Render(finishPart.Details)
227+ title := fmt.Sprintf("%s %s", errTag, a.sty.Messages.ErrorTitle.Render(truncated))
228+ details := a.sty.Messages.ErrorDetails.Width(width - 2).Render(finishPart.Details)
229229 return fmt.Sprintf("%s\n\n%s", title, details)
230230}
231231
internal/ui/chat/docker_mcp.go+5−5
@@ -173,7 +173,7 @@ func (d *DockerMCPToolRenderContext) renderMCPServers(sty *styles.Styles, opts *
173173 }
174174
175175 if len(result.Servers) == 0 {
176− return sty.Subtle.Render("No MCP servers found.")
176+ return sty.Tool.ResultEmpty.Render("No MCP servers found.")
177177 }
178178
179179 bodyWidth := min(120, width) - toolBodyLeftPaddingTotal
@@ -181,10 +181,10 @@ func (d *DockerMCPToolRenderContext) renderMCPServers(sty *styles.Styles, opts *
181181 moreServers := ""
182182 for i, server := range result.Servers {
183183 if i > 9 {
184− moreServers = sty.Subtle.Render(fmt.Sprintf("... and %d more", len(result.Servers)-10))
184+ moreServers = sty.Tool.ResultTruncation.Render(fmt.Sprintf("... and %d more", len(result.Servers)-10))
185185 break
186186 }
187− rows = append(rows, []string{sty.Base.Render(server.Name), sty.Subtle.Render(server.Description)})
187+ rows = append(rows, []string{sty.Tool.ResultItemName.Render(server.Name), sty.Tool.ResultItemDesc.Render(server.Description)})
188188 }
189189 serverTable := table.New().
190190 Wrap(false).
@@ -236,10 +236,10 @@ func (d *DockerMCPToolRenderContext) formatToolName(sty *styles.Styles, tool str
236236 action = "Find"
237237 case "mcp-add":
238238 action = "Add"
239− actionStyle = sty.Tool.DockerMCPActionAdd
239+ actionStyle = sty.Tool.ActionCreate
240240 case "mcp-remove":
241241 action = "Remove"
242− actionStyle = sty.Tool.DockerMCPActionDel
242+ actionStyle = sty.Tool.ActionDestroy
243243 case "code-mode":
244244 action = "Code Mode"
245245 default:
internal/ui/chat/messages.go+5−5
@@ -221,7 +221,7 @@ func (a *AssistantInfoItem) RawRender(width int) string {
221221
222222// Render implements MessageItem.
223223func (a *AssistantInfoItem) Render(width int) string {
224− prefix := a.sty.Chat.Message.SectionHeader.Render()
224+ prefix := a.sty.Messages.SectionHeader.Render()
225225 lines := strings.Split(a.RawRender(width), "\n")
226226 for i, line := range lines {
227227 lines[i] = prefix + line
@@ -236,18 +236,18 @@ func (a *AssistantInfoItem) renderContent(width int) string {
236236 }
237237 finishTime := time.Unix(finishData.Time, 0)
238238 duration := finishTime.Sub(a.lastUserMessageTime)
239− infoMsg := a.sty.Chat.Message.AssistantInfoDuration.Render(duration.String())
240− icon := a.sty.Chat.Message.AssistantInfoIcon.Render(styles.ModelIcon)
239+ infoMsg := a.sty.Messages.AssistantInfoDuration.Render(duration.String())
240+ icon := a.sty.Messages.AssistantInfoIcon.Render(styles.ModelIcon)
241241 model := a.cfg.GetModel(a.message.Provider, a.message.Model)
242242 if model == nil {
243243 model = &catwalk.Model{Name: "Unknown Model"}
244244 }
245− modelFormatted := a.sty.Chat.Message.AssistantInfoModel.Render(model.Name)
245+ modelFormatted := a.sty.Messages.AssistantInfoModel.Render(model.Name)
246246 providerName := a.message.Provider
247247 if providerConfig, ok := a.cfg.Providers.Get(a.message.Provider); ok {
248248 providerName = providerConfig.Name
249249 }
250− provider := a.sty.Chat.Message.AssistantInfoProvider.Render(fmt.Sprintf("via %s", providerName))
250+ provider := a.sty.Messages.AssistantInfoProvider.Render(fmt.Sprintf("via %s", providerName))
251251 assistant := fmt.Sprintf("%s %s %s %s", icon, modelFormatted, provider, infoMsg)
252252 return common.Section(a.sty, assistant, width)
253253}
internal/ui/chat/todos.go+6−6
@@ -91,16 +91,16 @@ func (t *TodosToolRenderContext) RenderTool(sty *styles.Styles, width int, opts
9191
9292 ratio := sty.Tool.TodoRatio.Render(fmt.Sprintf("%d/%d", meta.Completed, meta.Total))
9393 if hasCompleted && hasStarted {
94− text := sty.Subtle.Render(fmt.Sprintf(" · completed %d, starting next", len(meta.JustCompleted)))
94+ text := sty.Tool.TodoStatusNote.Render(fmt.Sprintf(" · completed %d, starting next", len(meta.JustCompleted)))
9595 headerText = fmt.Sprintf("%s%s", ratio, text)
9696 } else if hasCompleted {
97− text := sty.Subtle.Render(fmt.Sprintf(" · completed %d", len(meta.JustCompleted)))
97+ text := sty.Tool.TodoStatusNote.Render(fmt.Sprintf(" · completed %d", len(meta.JustCompleted)))
9898 if allCompleted {
99− text = sty.Subtle.Render(" · completed all")
99+ text = sty.Tool.TodoStatusNote.Render(" · completed all")
100100 }
101101 headerText = fmt.Sprintf("%s%s", ratio, text)
102102 } else if hasStarted {
103− headerText = fmt.Sprintf("%s%s", ratio, sty.Subtle.Render(" · starting task"))
103+ headerText = fmt.Sprintf("%s%s", ratio, sty.Tool.TodoStatusNote.Render(" · starting task"))
104104 } else {
105105 headerText = ratio
106106 }
@@ -111,7 +111,7 @@ func (t *TodosToolRenderContext) RenderTool(sty *styles.Styles, width int, opts
111111 body = FormatTodosList(sty, meta.Todos, styles.ArrowRightIcon, cappedWidth)
112112 } else if meta.JustStarted != "" {
113113 body = sty.Tool.TodoInProgressIcon.Render(styles.ArrowRightIcon+" ") +
114− sty.Base.Render(meta.JustStarted)
114+ sty.Tool.TodoJustStarted.Render(meta.JustStarted)
115115 }
116116 }
117117 }
@@ -148,7 +148,7 @@ func FormatTodosList(sty *styles.Styles, todos []session.Todo, inProgressIcon st
148148 var lines []string
149149 for _, todo := range sorted {
150150 var prefix string
151− textStyle := sty.Base
151+ textStyle := sty.Tool.TodoItem
152152
153153 switch todo.Status {
154154 case session.TodoStatusCompleted:
internal/ui/chat/tools.go+7−7
@@ -189,9 +189,9 @@ func newBaseToolMessageItem(
189189 t.anim = anim.New(anim.Settings{
190190 ID: toolCall.ID,
191191 Size: 15,
192− GradColorA: sty.Primary,
193− GradColorB: sty.Secondary,
194− LabelColor: sty.FgBase,
192+ GradColorA: sty.WorkingGradFromColor,
193+ GradColorB: sty.WorkingGradToColor,
194+ LabelColor: sty.WorkingLabelColor,
195195 CycleColors: true,
196196 })
197197
@@ -325,11 +325,11 @@ func (t *baseToolMessageItem) RawRender(width int) string {
325325func (t *baseToolMessageItem) Render(width int) string {
326326 var prefix string
327327 if t.isCompact {
328− prefix = t.sty.Chat.Message.ToolCallCompact.Render()
328+ prefix = t.sty.Messages.ToolCallCompact.Render()
329329 } else if t.focused {
330− prefix = t.sty.Chat.Message.ToolCallFocused.Render()
330+ prefix = t.sty.Messages.ToolCallFocused.Render()
331331 } else {
332− prefix = t.sty.Chat.Message.ToolCallBlurred.Render()
332+ prefix = t.sty.Messages.ToolCallBlurred.Render()
333333 }
334334 lines := strings.Split(t.RawRender(width), "\n")
335335 for i, ln := range lines {
@@ -799,7 +799,7 @@ func toolOutputMarkdownContent(sty *styles.Styles, content string, width int, ex
799799 width = maxTextWidth
800800 }
801801
802− renderer := common.PlainMarkdownRenderer(sty, width)
802+ renderer := common.QuietMarkdownRenderer(sty, width)
803803 rendered, err := renderer.Render(content)
804804 if err != nil {
805805 return toolOutputPlainContent(sty, content, width, expanded)
internal/ui/chat/user.go+2−2
@@ -72,9 +72,9 @@ func (m *UserMessageItem) RawRender(width int) string {
7272func (m *UserMessageItem) Render(width int) string {
7373 var prefix string
7474 if m.focused {
75− prefix = m.sty.Chat.Message.UserFocused.Render()
75+ prefix = m.sty.Messages.UserFocused.Render()
7676 } else {
77− prefix = m.sty.Chat.Message.UserBlurred.Render()
77+ prefix = m.sty.Messages.UserBlurred.Render()
7878 }
7979 lines := strings.Split(m.RawRender(width), "\n")
8080 for i, line := range lines {
internal/ui/common/button.go+2−2
@@ -22,9 +22,9 @@ type ButtonOpts struct {
2222// Button creates a button with an underlined character and selection state
2323func Button(t *styles.Styles, opts ButtonOpts) string {
2424 // Select style based on selection state
25− style := t.ButtonBlur
25+ style := t.Button.Blurred
2626 if opts.Selected {
27− style = t.ButtonFocus
27+ style = t.Button.Focused
2828 }
2929
3030 text := opts.Text
internal/ui/common/elements.go+14−14
@@ -18,7 +18,7 @@ import (
1818// muted styling.
1919func PrettyPath(t *styles.Styles, path string, width int) string {
2020 formatted := home.Short(path)
21− return t.Muted.Width(width).Render(formatted)
21+ return t.Sidebar.WorkingDir.Width(width).Render(formatted)
2222}
2323
2424// FormatReasoningEffort formats a reasoning effort level for display.
@@ -39,13 +39,13 @@ type ModelContextInfo struct {
3939// ModelInfo renders model information including name, provider, reasoning
4040// settings, and optional context usage/cost.
4141func ModelInfo(t *styles.Styles, modelName, providerName, reasoningInfo string, context *ModelContextInfo, width int) string {
42− modelIcon := t.Subtle.Render(styles.ModelIcon)
43− modelName = t.Base.Render(modelName)
42+ modelIcon := t.ModelInfo.Icon.Render(styles.ModelIcon)
43+ modelName = t.ModelInfo.Name.Render(modelName)
4444
4545 // Build first line with model name and optionally provider on the same line
4646 var firstLine string
4747 if providerName != "" {
48− providerInfo := t.Muted.Render(fmt.Sprintf("via %s", providerName))
48+ providerInfo := t.ModelInfo.Provider.Render(fmt.Sprintf("via %s", providerName))
4949 modelWithProvider := fmt.Sprintf("%s %s %s", modelIcon, modelName, providerInfo)
5050
5151 // Check if it fits on one line
@@ -64,11 +64,11 @@ func ModelInfo(t *styles.Styles, modelName, providerName, reasoningInfo string,
6464 // If provider didn't fit on first line, add it as second line
6565 if providerName != "" && !strings.Contains(firstLine, "via") {
6666 providerInfo := fmt.Sprintf("via %s", providerName)
67− parts = append(parts, t.Muted.PaddingLeft(2).Render(providerInfo))
67+ parts = append(parts, t.ModelInfo.ProviderFallback.Render(providerInfo))
6868 }
6969
7070 if reasoningInfo != "" {
71− parts = append(parts, t.Subtle.PaddingLeft(2).Render(reasoningInfo))
71+ parts = append(parts, t.ModelInfo.Reasoning.Render(reasoningInfo))
7272 }
7373
7474 if context != nil {
@@ -103,10 +103,10 @@ func formatTokensAndCost(t *styles.Styles, tokens, contextWindow int64, cost flo
103103
104104 percentage := (float64(tokens) / float64(contextWindow)) * 100
105105
106− formattedCost := t.Muted.Render(fmt.Sprintf("$%.2f", cost))
106+ formattedCost := t.ModelInfo.Cost.Render(fmt.Sprintf("$%.2f", cost))
107107
108− formattedTokens = t.Subtle.Render(fmt.Sprintf("(%s)", formattedTokens))
109− formattedPercentage := t.Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
108+ formattedTokens = t.ModelInfo.TokenCount.Render(fmt.Sprintf("(%s)", formattedTokens))
109+ formattedPercentage := t.ModelInfo.TokenPercentage.Render(fmt.Sprintf("%d%%", int(percentage)))
110110 formattedTokens = fmt.Sprintf("%s %s", formattedPercentage, formattedTokens)
111111 if percentage > 80 {
112112 formattedTokens = fmt.Sprintf("%s %s", styles.LSPWarningIcon, formattedTokens)
@@ -133,10 +133,10 @@ func Status(t *styles.Styles, opts StatusOpts, width int) string {
133133 title := opts.Title
134134 description := opts.Description
135135
136− titleColor := cmp.Or(opts.TitleColor, t.Muted.GetForeground())
137− descriptionColor := cmp.Or(opts.DescriptionColor, t.Subtle.GetForeground())
136+ titleColor := cmp.Or(opts.TitleColor, t.Resource.DefaultTitleFg)
137+ descriptionColor := cmp.Or(opts.DescriptionColor, t.Resource.DefaultDescFg)
138138
139− title = t.Base.Foreground(titleColor).Render(title)
139+ title = t.Resource.RowTitleBase.Foreground(titleColor).Render(title)
140140
141141 if description != "" {
142142 extraContentWidth := lipgloss.Width(opts.ExtraContent)
@@ -144,7 +144,7 @@ func Status(t *styles.Styles, opts StatusOpts, width int) string {
144144 extraContentWidth += 1
145145 }
146146 description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContentWidth, "…")
147− description = t.Base.Foreground(descriptionColor).Render(description)
147+ description = t.Resource.RowDescBase.Foreground(descriptionColor).Render(description)
148148 }
149149
150150 var content []string
@@ -193,7 +193,7 @@ func DialogTitle(t *styles.Styles, title string, width int, fromColor, toColor c
193193 remainingWidth := width - length
194194 if remainingWidth > 0 {
195195 lines := strings.Repeat(char, remainingWidth)
196− lines = styles.ApplyForegroundGrad(t, lines, fromColor, toColor)
196+ lines = styles.ApplyForegroundGrad(t.Dialog.TitleLineBase, lines, fromColor, toColor)
197197 title = title + " " + lines
198198 }
199199 return title
internal/ui/common/markdown.go+3−3
@@ -29,11 +29,11 @@ func MarkdownRenderer(sty *styles.Styles, width int) *glamour.TermRenderer {
2929 return r
3030}
3131
32−// PlainMarkdownRenderer returns a glamour [glamour.TermRenderer] with no colors
32+// QuietMarkdownRenderer returns a glamour [glamour.TermRenderer] with no colors
3333// (plain text with structure) and the given width.
34−func PlainMarkdownRenderer(sty *styles.Styles, width int) *glamour.TermRenderer {
34+func QuietMarkdownRenderer(sty *styles.Styles, width int) *glamour.TermRenderer {
3535 r, _ := glamour.NewTermRenderer(
36− glamour.WithStyles(sty.PlainMarkdown),
36+ glamour.WithStyles(sty.QuietMarkdown),
3737 glamour.WithWordWrap(width),
3838 glamour.WithChromaFormatter(formatterName),
3939 )
internal/ui/dialog/api_key_input.go+2−2
@@ -83,7 +83,7 @@ func NewAPIKeyInput(
8383
8484 m.spinner = spinner.New(
8585 spinner.WithSpinner(spinner.Dot),
86− spinner.WithStyle(t.Base.Foreground(t.Green)),
86+ spinner.WithStyle(t.Dialog.APIKey.Spinner),
8787 )
8888
8989 m.help = help.New()
@@ -202,7 +202,7 @@ func (m *APIKeyInput) headerView() string {
202202 return textStyle.Render(m.dialogTitle())
203203 }
204204 headerOffset := titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize()
205− return common.DialogTitle(t, titleStyle.Render(m.dialogTitle()), m.width-headerOffset, m.com.Styles.Primary, m.com.Styles.Secondary)
205+ return common.DialogTitle(t, titleStyle.Render(m.dialogTitle()), m.width-headerOffset, m.com.Styles.Dialog.TitleGradFromColor, m.com.Styles.Dialog.TitleGradToColor)
206206}
207207
208208func (m *APIKeyInput) dialogTitle() string {
internal/ui/dialog/arguments.go+1−1
@@ -314,7 +314,7 @@ func (a *Arguments) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
314314
315315 titleText := cmp.Or(a.title, "Arguments")
316316
317− header := common.DialogTitle(s, titleText, width, s.Primary, s.Secondary)
317+ header := common.DialogTitle(s, titleText, width, s.Dialog.TitleGradFromColor, s.Dialog.TitleGradToColor)
318318
319319 // Add description if available.
320320 var description string
internal/ui/dialog/commands.go+2−2
@@ -252,9 +252,9 @@ func commandsRadioView(sty *styles.Styles, selected CommandType, hasUserCmds boo
252252
253253 selectedFn := func(t CommandType) string {
254254 if t == selected {
255− return sty.RadioOn.Padding(0, 1).Render() + sty.HalfMuted.Render(t.String())
255+ return sty.Radio.On.Padding(0, 1).Render() + sty.Radio.Label.Render(t.String())
256256 }
257− return sty.RadioOff.Padding(0, 1).Render() + sty.HalfMuted.Render(t.String())
257+ return sty.Radio.Off.Padding(0, 1).Render() + sty.Radio.Label.Render(t.String())
258258 }
259259
260260 parts := []string{
internal/ui/dialog/commands_item.go+2−2
@@ -69,8 +69,8 @@ func (c *CommandItem) Render(width int) string {
6969 styles := ListItemStyles{
7070 ItemBlurred: c.t.Dialog.NormalItem,
7171 ItemFocused: c.t.Dialog.SelectedItem,
72− InfoTextBlurred: c.t.Base,
73− InfoTextFocused: c.t.Base,
72+ InfoTextBlurred: c.t.Dialog.ListItem.InfoBlurred,
73+ InfoTextFocused: c.t.Dialog.ListItem.InfoFocused,
7474 }
7575 return renderItem(styles, c.title, c.shortcut, c.focused, width, c.cache, &c.m)
7676}
internal/ui/dialog/common.go+6−4
@@ -65,9 +65,11 @@ type RenderContext struct {
6565 TitleStyle lipgloss.Style
6666 // ViewStyle is the style of the dialog title by default it uses Styles.Dialog.View
6767 ViewStyle lipgloss.Style
68− // TitleGradientFromColor is the color the title gradient starts by defaults its Style.Primary
68+ // TitleGradientFromColor is the color the title gradient starts by default
69+ // its Styles.Dialog.TitleGradFromColor
6970 TitleGradientFromColor color.Color
70− // TitleGradientToColor is the color the title gradient starts by defaults its Style.Secondary
71+ // TitleGradientToColor is the color the title gradient ends by default its
72+ // Styles.Dialog.TitleGradToColor
7173 TitleGradientToColor color.Color
7274 // Width is the total width of the dialog including any margins, borders,
7375 // and paddings.
@@ -98,8 +100,8 @@ func NewRenderContext(t *styles.Styles, width int) *RenderContext {
98100 Styles: t,
99101 TitleStyle: t.Dialog.Title,
100102 ViewStyle: t.Dialog.View,
101− TitleGradientFromColor: t.Primary,
102− TitleGradientToColor: t.Secondary,
103+ TitleGradientFromColor: t.Dialog.TitleGradFromColor,
104+ TitleGradientToColor: t.Dialog.TitleGradToColor,
103105 Width: width,
104106 Parts: []string{},
105107 }
internal/ui/dialog/models.go+5−5
@@ -236,13 +236,13 @@ func (m *Models) Cursor() *tea.Cursor {
236236// modelTypeRadioView returns the radio view for model type selection.
237237func (m *Models) modelTypeRadioView() string {
238238 t := m.com.Styles
239− textStyle := t.HalfMuted
240− largeRadioStyle := t.RadioOff
241− smallRadioStyle := t.RadioOff
239+ textStyle := t.Radio.Label
240+ largeRadioStyle := t.Radio.Off
241+ smallRadioStyle := t.Radio.Off
242242 if m.modelType == ModelTypeLarge {
243− largeRadioStyle = t.RadioOn
243+ largeRadioStyle = t.Radio.On
244244 } else {
245− smallRadioStyle = t.RadioOn
245+ smallRadioStyle = t.Radio.On
246246 }
247247
248248 largeRadio := largeRadioStyle.Padding(0, 1).Render()
internal/ui/dialog/models_item.go+3−3
@@ -38,7 +38,7 @@ func (m *ModelGroup) Render(width int) string {
3838 var configured string
3939 if m.configured {
4040 configuredIcon := m.t.ToolCallSuccess.Render()
41− configuredText := m.t.Subtle.Render("Configured")
41+ configuredText := m.t.Dialog.Models.ConfiguredText.Render("Configured")
4242 configured = configuredIcon + " " + configuredText
4343 }
4444
@@ -109,8 +109,8 @@ func (m *ModelItem) Render(width int) string {
109109 styles := ListItemStyles{
110110 ItemBlurred: m.t.Dialog.NormalItem,
111111 ItemFocused: m.t.Dialog.SelectedItem,
112− InfoTextBlurred: m.t.Base,
113− InfoTextFocused: m.t.Base,
112+ InfoTextBlurred: m.t.Dialog.ListItem.InfoBlurred,
113+ InfoTextFocused: m.t.Dialog.ListItem.InfoFocused,
114114 }
115115 return renderItem(styles, m.model.Name, providerInfo, m.focused, width, m.cache, &m.m)
116116}
internal/ui/dialog/oauth.go+10−13
@@ -94,7 +94,7 @@ func newOAuth(
9494
9595 m.spinner = spinner.New(
9696 spinner.WithSpinner(spinner.Dot),
97− spinner.WithStyle(t.Base.Foreground(t.GreenLight)),
97+ spinner.WithStyle(t.Dialog.OAuth.Spinner),
9898 )
9999
100100 m.help = help.New()
@@ -227,18 +227,18 @@ func (m *OAuth) headerContent() string {
227227 if m.isOnboarding {
228228 return textStyle.Render(dialogTitle)
229229 }
230− return common.DialogTitle(t, titleStyle.Render(dialogTitle), m.width-headerOffset, t.Primary, t.Secondary)
230+ return common.DialogTitle(t, titleStyle.Render(dialogTitle), m.width-headerOffset, t.Dialog.TitleGradFromColor, t.Dialog.TitleGradToColor)
231231}
232232
233233func (m *OAuth) innerDialogContent() string {
234234 var (
235235 t = m.com.Styles
236− whiteStyle = lipgloss.NewStyle().Foreground(t.White)
237− primaryStyle = lipgloss.NewStyle().Foreground(t.Primary)
238− greenStyle = lipgloss.NewStyle().Foreground(t.GreenLight)
239− linkStyle = lipgloss.NewStyle().Foreground(t.GreenDark).Underline(true)
240− errorStyle = lipgloss.NewStyle().Foreground(t.Error)
241− mutedStyle = lipgloss.NewStyle().Foreground(t.FgMuted)
236+ whiteStyle = t.Dialog.OAuth.Instructions
237+ primaryStyle = t.Dialog.OAuth.Enter
238+ greenStyle = t.Dialog.OAuth.Success
239+ linkStyle = t.Dialog.OAuth.Link
240+ errorStyle = t.Dialog.OAuth.ErrorText
241+ mutedStyle = t.Dialog.OAuth.StatusText
242242 )
243243
244244 switch m.State {
@@ -266,13 +266,10 @@ func (m *OAuth) innerDialogContent() string {
266266 Width(m.width-2).
267267 Height(7).
268268 Align(lipgloss.Center, lipgloss.Center).
269− Background(t.BgBaseLighter).
269+ Background(t.Dialog.OAuth.UserCodeBg).
270270 Margin(0, 1).
271271 Render(
272− lipgloss.NewStyle().
273− Bold(true).
274− Foreground(t.White).
275− Render(m.userCode),
272+ t.Dialog.OAuth.UserCode.Render(m.userCode),
276273 )
277274
278275 link := linkStyle.Hyperlink(m.verificationURL, "id=oauth-verify").Render(m.verificationURL)
internal/ui/dialog/permissions.go+4−4
@@ -443,7 +443,7 @@ func (p *Permissions) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
443443func (p *Permissions) renderHeader(contentWidth int) string {
444444 t := p.com.Styles
445445
446− title := common.DialogTitle(t, "Permission Required", contentWidth-t.Dialog.Title.GetHorizontalFrameSize(), t.Primary, t.Secondary)
446+ title := common.DialogTitle(t, "Permission Required", contentWidth-t.Dialog.Title.GetHorizontalFrameSize(), t.Dialog.TitleGradFromColor, t.Dialog.TitleGradToColor)
447447 title = t.Dialog.Title.Render(title)
448448
449449 // Tool info.
@@ -489,8 +489,8 @@ func (p *Permissions) renderHeader(contentWidth int) string {
489489
490490func (p *Permissions) renderKeyValue(key, value string, width int) string {
491491 t := p.com.Styles
492− keyStyle := t.Muted
493− valueStyle := t.Base
492+ keyStyle := t.Dialog.Permissions.KeyText
493+ valueStyle := t.Dialog.Permissions.ValueText
494494
495495 keyStr := keyStyle.Render(key)
496496 valueStr := valueStyle.Width(width - lipgloss.Width(keyStr) - 1).Render(" " + value)
@@ -699,7 +699,7 @@ func (p *Permissions) renderDefaultContent(width int) string {
699699 if err := json.Unmarshal([]byte(paramStr), &parsed); err == nil {
700700 if b, err := json.MarshalIndent(parsed, "", " "); err == nil {
701701 jsonContent := string(b)
702− highlighted, err := common.SyntaxHighlight(t, jsonContent, "params.json", t.BgSubtle)
702+ highlighted, err := common.SyntaxHighlight(t, jsonContent, "params.json", t.Dialog.Permissions.ParamsBg)
703703 if err == nil {
704704 jsonContent = highlighted
705705 }
internal/ui/dialog/quit.go+2−2
@@ -96,7 +96,7 @@ func (q *Quit) HandleMsg(msg tea.Msg) Action {
9696// Draw implements [Dialog].
9797func (q *Quit) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
9898 const question = "Are you sure you want to quit?"
99− baseStyle := q.com.Styles.Base
99+ baseStyle := q.com.Styles.Dialog.Quit.Content
100100 buttonOpts := []common.ButtonOpts{
101101 {Text: "Yep!", Selected: !q.selectedNo, Padding: 3},
102102 {Text: "Nope", Selected: q.selectedNo, Padding: 3},
@@ -111,7 +111,7 @@ func (q *Quit) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
111111 ),
112112 )
113113
114− view := q.com.Styles.BorderFocus.Render(content)
114+ view := q.com.Styles.Dialog.Quit.Frame.Render(content)
115115 DrawCenter(scr, area, view)
116116 return nil
117117}
internal/ui/dialog/reasoning.go+2−2
@@ -293,8 +293,8 @@ func (r *ReasoningItem) Render(width int) string {
293293 styles := ListItemStyles{
294294 ItemBlurred: r.t.Dialog.NormalItem,
295295 ItemFocused: r.t.Dialog.SelectedItem,
296− InfoTextBlurred: r.t.Base,
297− InfoTextFocused: r.t.Base,
296+ InfoTextBlurred: r.t.Dialog.ListItem.InfoBlurred,
297+ InfoTextFocused: r.t.Dialog.ListItem.InfoFocused,
298298 }
299299 return renderItem(styles, r.title, info, r.focused, width, r.cache, &r.m)
300300}
internal/ui/dialog/sessions_item.go+2−2
@@ -79,8 +79,8 @@ func (s *SessionItem) Render(width int) string {
7979 styles := ListItemStyles{
8080 ItemBlurred: s.t.Dialog.NormalItem,
8181 ItemFocused: s.t.Dialog.SelectedItem,
82− InfoTextBlurred: s.t.Subtle,
83− InfoTextFocused: s.t.Base,
82+ InfoTextBlurred: s.t.Dialog.Sessions.InfoBlurred,
83+ InfoTextFocused: s.t.Dialog.Sessions.InfoFocused,
8484 }
8585
8686 switch s.sessionsMode {
internal/ui/logo/example/main.gonew file+49
@@ -0,0 +1,49 @@
1+package main
2+
3+// This is an example for testing logo treatments. Do not remove.
4+
5+import (
6+ "fmt"
7+ "os"
8+
9+ "charm.land/lipgloss/v2"
10+ "github.com/charmbracelet/crush/internal/ui/logo"
11+ "github.com/charmbracelet/crush/internal/ui/styles"
12+ "github.com/charmbracelet/x/term"
13+)
14+
15+func main() {
16+ w, _, err := term.GetSize(os.Stdout.Fd())
17+ if err != nil {
18+ fmt.Fprintf(os.Stderr, "Could not get terminal size: %s", err)
19+ }
20+
21+ s := styles.DefaultStyles()
22+ opts := logo.Opts{
23+ FieldColor: s.Logo.FieldColor,
24+ TitleColorA: s.Logo.TitleColorA,
25+ TitleColorB: s.Logo.TitleColorB,
26+ CharmColor: s.Logo.CharmColor,
27+ VersionColor: s.Logo.VersionColor,
28+ Width: w,
29+ Unstable: true,
30+ }
31+
32+ renderCompact := func(hyper bool) string {
33+ opts.Hyper = hyper
34+ return logo.Render(s.Logo.GradCanvas, "v1.0.0", true, opts)
35+ }
36+
37+ renderWide := func(hyper bool) string {
38+ opts.Hyper = hyper
39+ return logo.Render(s.Logo.GradCanvas, "v1.0.0", false, opts)
40+ }
41+
42+ lipgloss.Println(
43+ lipgloss.JoinHorizontal(lipgloss.Top, renderCompact(false), " ", renderCompact(true)),
44+ )
45+
46+ for i := range 6 {
47+ lipgloss.Println(renderWide(i > 0))
48+ }
49+}
internal/ui/logo/letterforms.gonew file+419
@@ -0,0 +1,419 @@
1+package logo
2+
3+import (
4+ "strings"
5+
6+ "charm.land/lipgloss/v2"
7+ "github.com/MakeNowJust/heredoc"
8+ "github.com/charmbracelet/x/exp/slice"
9+)
10+
11+// renderWord renders letterforms to fork a word. stretchIndex is the index of
12+// the letter to stretch, or -1 if no letter should be stretched.
13+func renderWord(spacing int, stretchIndex int, letterforms ...letterform) string {
14+ if spacing < 0 {
15+ spacing = 0
16+ }
17+
18+ renderedLetterforms := make([]string, len(letterforms))
19+
20+ // pick one letter randomly to stretch
21+ for i, letter := range letterforms {
22+ renderedLetterforms[i] = letter(i == stretchIndex)
23+ }
24+
25+ if spacing > 0 {
26+ // Add spaces between the letters and render.
27+ renderedLetterforms = slice.Intersperse(renderedLetterforms, strings.Repeat(" ", spacing))
28+ }
29+ return strings.TrimSpace(
30+ lipgloss.JoinHorizontal(lipgloss.Top, renderedLetterforms...),
31+ )
32+}
33+
34+// LetterC renders the letter C in a stylized way. It takes an integer that
35+// determines how many cells to stretch the letter. If the stretch is less than
36+// 1, it defaults to no stretching.
37+func LetterC(stretch bool) string {
38+ // Here's what we're making:
39+ //
40+ // ▄▀▀▀▀
41+ // █
42+ // ▀▀▀▀
43+
44+ left := heredoc.Doc(`
45+ ▄
46+ █
47+ `)
48+ right := heredoc.Doc(`
49+ ▀
50+
51+ ▀
52+ `)
53+ return joinLetterform(
54+ left,
55+ stretchLetterformPart(right, letterformProps{
56+ stretch: stretch,
57+ width: 4,
58+ minStretch: 7,
59+ maxStretch: 12,
60+ }),
61+ )
62+}
63+
64+// LetterE renders the letter E in a stylized way. It takes an integer that
65+// determines how many cells to stretch the letter. If the stretch is less than
66+// 1, it defaults to no stretching.
67+//
68+// This is an alternate letterform. DO NOT REMOVE.
69+func LetterE(stretch bool) string {
70+ // Here's what we're making:
71+ //
72+ // █▀▀▀▀
73+ // █▀▀▀▀
74+ // ▀▀▀▀▀
75+
76+ left := heredoc.Doc(`
77+ █
78+ █
79+ ▀
80+ `)
81+ middle := heredoc.Doc(`
82+ ▀
83+ ▀
84+ ▀
85+ `)
86+ return joinLetterform(
87+ left,
88+ stretchLetterformPart(middle, letterformProps{
89+ stretch: stretch,
90+ width: 4,
91+ minStretch: 7,
92+ maxStretch: 12,
93+ }),
94+ )
95+}
96+
97+// LetterEAlt renders the letter E in a stylized way. It takes an integer that
98+// determines how many cells to stretch the letter. If the stretch is less than
99+// 1, it defaults to no stretching.
100+//
101+// This is an alternate letterform. DO NOT REMOVE.
102+func LetterEAlt(stretch bool) string {
103+ // Here's what we're making:
104+ //
105+ // █▀▀▀▀
106+ // █ ▀▀▀
107+ // ▀▀▀▀▀
108+
109+ left := heredoc.Doc(`
110+ █▀
111+ █
112+ ▀▀
113+ `)
114+ middle := heredoc.Doc(`
115+ ▀
116+ ▀
117+ ▀
118+ `)
119+ return joinLetterform(
120+ left,
121+ stretchLetterformPart(middle, letterformProps{
122+ stretch: stretch,
123+ width: 3,
124+ minStretch: 6,
125+ maxStretch: 11,
126+ }),
127+ )
128+}
129+
130+// LetterH renders the letter H in a stylized way. It takes an integer that
131+// determines how many cells to stretch the letter. If the stretch is less than
132+// 1, it defaults to no stretching.
133+func LetterH(stretch bool) string {
134+ // Here's what we're making:
135+ //
136+ // █ █
137+ // █▀▀▀█
138+ // ▀ ▀
139+
140+ side := heredoc.Doc(`
141+ █
142+ █
143+ ▀`)
144+ middle := heredoc.Doc(`
145+
146+ ▀
147+ `)
148+ return joinLetterform(
149+ side,
150+ stretchLetterformPart(middle, letterformProps{
151+ stretch: stretch,
152+ width: 3,
153+ minStretch: 8,
154+ maxStretch: 12,
155+ }),
156+ side,
157+ )
158+}
159+
160+// LetterP renders the letter P in a stylized way. It takes an integer that
161+// determines how many cells to stretch the letter. If the stretch is less than
162+// 1, it defaults to no stretching.
163+func LetterP(stretch bool) string {
164+ // Here's what we're making:
165+ //
166+ // █▀▀▀▄
167+ // █▀▀▀
168+ // ▀
169+
170+ left := heredoc.Doc(`
171+ █
172+ █
173+ ▀
174+ `)
175+ center := heredoc.Doc(`
176+ ▀
177+ ▀
178+ `)
179+ right := heredoc.Doc(`
180+ ▄
181+
182+
183+ `)
184+ return joinLetterform(
185+ left,
186+ stretchLetterformPart(center, letterformProps{
187+ stretch: stretch,
188+ width: 3,
189+ minStretch: 7,
190+ maxStretch: 12,
191+ }),
192+ right,
193+ )
194+}
195+
196+// LetterR renders the letter R in a stylized way. It takes an integer that
197+// determines how many cells to stretch the letter. If the stretch is less than
198+// 1, it defaults to no stretching.
199+func LetterR(stretch bool) string {
200+ // Here's what we're making:
201+ //
202+ // █▀▀▀▄
203+ // █▀▀▀▄
204+ // ▀ ▀
205+
206+ left := heredoc.Doc(`
207+ █
208+ █
209+ ▀
210+ `)
211+ center := heredoc.Doc(`
212+ ▀
213+ ▀
214+ `)
215+ right := heredoc.Doc(`
216+ ▄
217+ ▄
218+ ▀
219+ `)
220+ return joinLetterform(
221+ left,
222+ stretchLetterformPart(center, letterformProps{
223+ stretch: stretch,
224+ width: 3,
225+ minStretch: 7,
226+ maxStretch: 12,
227+ }),
228+ right,
229+ )
230+}
231+
232+// LetterSAlt renders the letter S in a stylized way, more so than
233+// [letterS]. It takes an integer that determines how many cells to stretch the
234+// letter. If the stretch is less than 1, it defaults to no stretching.
235+//
236+// This is an alternate letterform. DO NOT REMOVE.
237+func LetterSAlt(stretch bool) string {
238+ // Here's what we're making:
239+ //
240+ // ▄▀▀▀▀▀
241+ // ▀▀▀▀▀█
242+ // ▀▀▀▀▀
243+
244+ left := heredoc.Doc(`
245+ ▄
246+ ▀
247+ ▀
248+ `)
249+ center := heredoc.Doc(`
250+ ▀
251+ ▀
252+ ▀
253+ `)
254+ right := heredoc.Doc(`
255+ ▀
256+ █
257+ `)
258+ return joinLetterform(
259+ left,
260+ stretchLetterformPart(center, letterformProps{
261+ stretch: stretch,
262+ width: 3,
263+ minStretch: 7,
264+ maxStretch: 12,
265+ }),
266+ right,
267+ )
268+}
269+
270+// LetterU renders the letter U in a stylized way. It takes an integer that
271+// determines how many cells to stretch the letter. If the stretch is less than
272+// 1, it defaults to no stretching.
273+func LetterU(stretch bool) string {
274+ // Here's what we're making:
275+ //
276+ // █ █
277+ // █ █
278+ // ▀▀▀
279+
280+ side := heredoc.Doc(`
281+ █
282+ █
283+ `)
284+ middle := heredoc.Doc(`
285+
286+
287+ ▀
288+ `)
289+ return joinLetterform(
290+ side,
291+ stretchLetterformPart(middle, letterformProps{
292+ stretch: stretch,
293+ width: 3,
294+ minStretch: 7,
295+ maxStretch: 12,
296+ }),
297+ side,
298+ )
299+}
300+
301+// LetterY renders the letter Y in a stylized way. It takes an integer that
302+// determines how many cells to stretch the letter. If the stretch is less than
303+// 1, it defaults to no stretching.
304+//
305+// This is an alternate letterform. DO NOT REMOVE.
306+func LetterY(stretch bool) string {
307+ // Here's what we're making:
308+ //
309+ // █ █
310+ // ▀▄▀
311+ // ▀
312+
313+ side := heredoc.Doc(`
314+ █
315+
316+ `)
317+ inside := heredoc.Doc(`
318+
319+ ▀
320+
321+ `)
322+ middle := heredoc.Doc(`
323+
324+ ▄
325+ ▀
326+ `)
327+ if stretch {
328+ middle = heredoc.Doc(`
329+
330+ █
331+ ▀
332+ `)
333+ }
334+
335+ stretchedInside := stretchLetterformPart(inside, letterformProps{
336+ stretch: stretch,
337+ width: 1,
338+ minStretch: 4,
339+ maxStretch: 10,
340+ })
341+
342+ return joinLetterform(
343+ side,
344+ stretchedInside,
345+ middle,
346+ stretchedInside,
347+ side,
348+ )
349+}
350+
351+// LetterYAlt renders the letter Y in a stylized way. It takes an integer that
352+// determines how many cells to stretch the letter. If the stretch is less than
353+// 1, it defaults to no stretching.
354+//
355+// This is an alternate letterform. DO NOT REMOVE.
356+func LetterYAlt(stretch bool) string {
357+ // Here's what we're making:
358+ //
359+ // █ █
360+ // ▀▀▀▀█
361+ // ▀▀▀▀
362+
363+ left := heredoc.Doc(`
364+ █
365+ ▀
366+ ▀
367+ `)
368+ middle := heredoc.Doc(`
369+
370+ ▀
371+ ▀
372+ `)
373+ right := heredoc.Doc(`
374+ █
375+ █
376+
377+ `)
378+
379+ return joinLetterform(
380+ left,
381+ stretchLetterformPart(middle, letterformProps{
382+ stretch: stretch,
383+ width: 3,
384+ minStretch: 6,
385+ maxStretch: 10,
386+ }),
387+ right,
388+ )
389+}
390+
391+func joinLetterform(letters ...string) string {
392+ return lipgloss.JoinHorizontal(lipgloss.Top, letters...)
393+}
394+
395+// letterformProps defines letterform stretching properties.
396+// for readability.
397+type letterformProps struct {
398+ width int
399+ minStretch int
400+ maxStretch int
401+ stretch bool
402+}
403+
404+// stretchLetterformPart is a helper function for letter stretching. If randomize
405+// is false the minimum number will be used.
406+func stretchLetterformPart(s string, p letterformProps) string {
407+ if p.maxStretch < p.minStretch {
408+ p.minStretch, p.maxStretch = p.maxStretch, p.minStretch
409+ }
410+ n := p.width
411+ if p.stretch {
412+ n = cachedRandN(p.maxStretch-p.minStretch) + p.minStretch //nolint:gosec
413+ }
414+ parts := make([]string, n)
415+ for i := range parts {
416+ parts[i] = s
417+ }
418+ return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
419+}
internal/ui/logo/logo.go+51−236
@@ -4,13 +4,12 @@ package logo
44import (
55 "fmt"
66 "image/color"
7+ "math/rand/v2"
78 "strings"
89
910 "charm.land/lipgloss/v2"
10− "github.com/MakeNowJust/heredoc"
1111 "github.com/charmbracelet/crush/internal/ui/styles"
1212 "github.com/charmbracelet/x/ansi"
13− "github.com/charmbracelet/x/exp/slice"
1413)
1514
1615// letterform represents a letterform. It can be stretched horizontally by
@@ -25,8 +24,14 @@ type Opts struct {
2524 TitleColorA color.Color // left gradient ramp point
2625 TitleColorB color.Color // right gradient ramp point
2726 CharmColor color.Color // Charm™ text color
28− VersionColor color.Color // Version text color
27+ VersionColor color.Color // version text color
2928 Width int // width of the rendered logo, used for truncation
29+ Hyper bool // whether it is Crush or Hypercrush
30+
31+ // When true, stretch a random letterform on each render. Has no effect in
32+ // compact mode. Mainly for testing. In production you will want to cache
33+ // the stretched letterform to keep the logo from jittering on resize.
34+ Unstable bool
3035}
3136
3237// Render renders the Crush logo. Set the argument to true to render the narrow
@@ -34,8 +39,11 @@ type Opts struct {
3439//
3540// The compact argument determines whether it renders compact for the sidebar
3641// or wider for the main pane.
37−func Render(s *styles.Styles, version string, compact bool, o Opts) string {
38− const charm = " Charm™"
42+func Render(base lipgloss.Style, version string, compact bool, o Opts) string {
43+ charm := "Charm™"
44+ if !o.Hyper {
45+ charm = " " + charm
46+ }
3947
4048 fg := func(c color.Color, s string) string {
4149 return lipgloss.NewStyle().Foreground(c).Render(s)
@@ -43,23 +51,43 @@ func Render(s *styles.Styles, version string, compact bool, o Opts) string {
4351
4452 // Title.
4553 const spacing = 1
46− letterforms := []letterform{
47− letterC,
48− letterR,
49− letterU,
50− letterSStylized,
51− letterH,
54+ var hyperLetterforms []letterform
55+ if o.Hyper {
56+ hyperLetterforms = []letterform{
57+ LetterH,
58+ LetterYAlt,
59+ LetterP,
60+ LetterE,
61+ LetterR,
62+ }
5263 }
53− stretchIndex := -1 // -1 means no stretching.
54− if !compact {
55− stretchIndex = cachedRandN(len(letterforms))
64+ crushLetterforms := []letterform{
65+ LetterC,
66+ LetterR,
67+ LetterU,
68+ LetterSAlt,
69+ LetterH,
70+ }
71+ if o.Hyper && !compact {
72+ crushLetterforms = append(hyperLetterforms, crushLetterforms...)
5673 }
5774
58− crush := renderWord(spacing, stretchIndex, letterforms...)
75+ stretchIndex := -1 // -1 means no stretching.
76+ if !compact && !o.Unstable {
77+ // Always stretch the same letterform, which is picked once at random.
78+ stretchIndex = cachedRandN(len(crushLetterforms))
79+ } else if !compact && o.Unstable {
80+ // Stretch a random letterform on every render.
81+ stretchIndex = rand.IntN(len(crushLetterforms))
82+ }
83+ crush := renderWord(spacing, stretchIndex, crushLetterforms...)
84+ if o.Hyper && compact {
85+ crush = renderWord(spacing, stretchIndex, hyperLetterforms...) + "\n" + crush
86+ }
5987 crushWidth := lipgloss.Width(crush)
6088 b := new(strings.Builder)
6189 for r := range strings.SplitSeq(crush, "\n") {
62− fmt.Fprintln(b, styles.ApplyForegroundGrad(s, r, o.TitleColorA, o.TitleColorB))
90+ fmt.Fprintln(b, styles.ApplyForegroundGrad(base, r, o.TitleColorA, o.TitleColorB))
6391 }
6492 crush = b.String()
6593
@@ -67,13 +95,16 @@ func Render(s *styles.Styles, version string, compact bool, o Opts) string {
6795 metaRowGap := 1
6896 maxVersionWidth := crushWidth - lipgloss.Width(charm) - metaRowGap
6997 version = ansi.Truncate(version, maxVersionWidth, "…") // truncate version if too long.
98+ if o.Hyper && compact {
99+ version += " "
100+ }
70101 gap := max(0, crushWidth-lipgloss.Width(charm)-lipgloss.Width(version))
71102 metaRow := fg(o.CharmColor, charm) + strings.Repeat(" ", gap) + fg(o.VersionColor, version)
72103
73104 // Join the meta row and big Crush title.
74105 crush = strings.TrimSpace(metaRow + "\n" + crush)
75106
76− // Narrow version.
107+ // Narrow version. If this is Hypercrush, this is also a stacked version.
77108 if compact {
78109 field := fg(o.FieldColor, strings.Repeat(diag, crushWidth))
79110 return strings.Join([]string{field, field, crush, field, ""}, "\n")
@@ -118,228 +149,12 @@ func Render(s *styles.Styles, version string, compact bool, o Opts) string {
118149// SmallRender renders a smaller version of the Crush logo, suitable for
119150// smaller windows or sidebar usage.
120151func SmallRender(t *styles.Styles, width int) string {
121− title := t.Base.Foreground(t.Secondary).Render("Charm™")
122− title = fmt.Sprintf("%s %s", title, styles.ApplyBoldForegroundGrad(t, "Crush", t.Secondary, t.Primary))
152+ title := t.Logo.SmallCharm.Render("Charm™")
153+ title = fmt.Sprintf("%s %s", title, styles.ApplyBoldForegroundGrad(t.Logo.GradCanvas, "Crush", t.Logo.SmallGradFromColor, t.Logo.SmallGradToColor))
123154 remainingWidth := width - lipgloss.Width(title) - 1 // 1 for the space after "Crush"
124155 if remainingWidth > 0 {
125156 lines := strings.Repeat("╱", remainingWidth)
126− title = fmt.Sprintf("%s %s", title, t.Base.Foreground(t.Primary).Render(lines))
157+ title = fmt.Sprintf("%s %s", title, t.Logo.SmallDiagonals.Render(lines))
127158 }
128159 return title
129160}
130−
131−// renderWord renders letterforms to fork a word. stretchIndex is the index of
132−// the letter to stretch, or -1 if no letter should be stretched.
133−func renderWord(spacing int, stretchIndex int, letterforms ...letterform) string {
134− if spacing < 0 {
135− spacing = 0
136− }
137−
138− renderedLetterforms := make([]string, len(letterforms))
139−
140− // pick one letter randomly to stretch
141− for i, letter := range letterforms {
142− renderedLetterforms[i] = letter(i == stretchIndex)
143− }
144−
145− if spacing > 0 {
146− // Add spaces between the letters and render.
147− renderedLetterforms = slice.Intersperse(renderedLetterforms, strings.Repeat(" ", spacing))
148− }
149− return strings.TrimSpace(
150− lipgloss.JoinHorizontal(lipgloss.Top, renderedLetterforms...),
151− )
152−}
153−
154−// letterC renders the letter C in a stylized way. It takes an integer that
155−// determines how many cells to stretch the letter. If the stretch is less than
156−// 1, it defaults to no stretching.
157−func letterC(stretch bool) string {
158− // Here's what we're making:
159− //
160− // ▄▀▀▀▀
161− // █
162− // ▀▀▀▀
163−
164− left := heredoc.Doc(`
165− ▄
166− █
167− `)
168− right := heredoc.Doc(`
169− ▀
170−
171− ▀
172− `)
173− return joinLetterform(
174− left,
175− stretchLetterformPart(right, letterformProps{
176− stretch: stretch,
177− width: 4,
178− minStretch: 7,
179− maxStretch: 12,
180− }),
181− )
182−}
183−
184−// letterH renders the letter H in a stylized way. It takes an integer that
185−// determines how many cells to stretch the letter. If the stretch is less than
186−// 1, it defaults to no stretching.
187−func letterH(stretch bool) string {
188− // Here's what we're making:
189− //
190− // █ █
191− // █▀▀▀█
192− // ▀ ▀
193−
194− side := heredoc.Doc(`
195− █
196− █
197− ▀`)
198− middle := heredoc.Doc(`
199−
200− ▀
201− `)
202− return joinLetterform(
203− side,
204− stretchLetterformPart(middle, letterformProps{
205− stretch: stretch,
206− width: 3,
207− minStretch: 8,
208− maxStretch: 12,
209− }),
210− side,
211− )
212−}
213−
214−// letterR renders the letter R in a stylized way. It takes an integer that
215−// determines how many cells to stretch the letter. If the stretch is less than
216−// 1, it defaults to no stretching.
217−func letterR(stretch bool) string {
218− // Here's what we're making:
219− //
220− // █▀▀▀▄
221− // █▀▀▀▄
222− // ▀ ▀
223−
224− left := heredoc.Doc(`
225− █
226− █
227− ▀
228− `)
229− center := heredoc.Doc(`
230− ▀
231− ▀
232− `)
233− right := heredoc.Doc(`
234− ▄
235− ▄
236− ▀
237− `)
238− return joinLetterform(
239− left,
240− stretchLetterformPart(center, letterformProps{
241− stretch: stretch,
242− width: 3,
243− minStretch: 7,
244− maxStretch: 12,
245− }),
246− right,
247− )
248−}
249−
250−// letterSStylized renders the letter S in a stylized way, more so than
251−// [letterS]. It takes an integer that determines how many cells to stretch the
252−// letter. If the stretch is less than 1, it defaults to no stretching.
253−func letterSStylized(stretch bool) string {
254− // Here's what we're making:
255− //
256− // ▄▀▀▀▀▀
257− // ▀▀▀▀▀█
258− // ▀▀▀▀▀
259−
260− left := heredoc.Doc(`
261− ▄
262− ▀
263− ▀
264− `)
265− center := heredoc.Doc(`
266− ▀
267− ▀
268− ▀
269− `)
270− right := heredoc.Doc(`
271− ▀
272− █
273− `)
274− return joinLetterform(
275− left,
276− stretchLetterformPart(center, letterformProps{
277− stretch: stretch,
278− width: 3,
279− minStretch: 7,
280− maxStretch: 12,
281− }),
282− right,
283− )
284−}
285−
286−// letterU renders the letter U in a stylized way. It takes an integer that
287−// determines how many cells to stretch the letter. If the stretch is less than
288−// 1, it defaults to no stretching.
289−func letterU(stretch bool) string {
290− // Here's what we're making:
291− //
292− // █ █
293− // █ █
294− // ▀▀▀
295−
296− side := heredoc.Doc(`
297− █
298− █
299− `)
300− middle := heredoc.Doc(`
301−
302−
303− ▀
304− `)
305− return joinLetterform(
306− side,
307− stretchLetterformPart(middle, letterformProps{
308− stretch: stretch,
309− width: 3,
310− minStretch: 7,
311− maxStretch: 12,
312− }),
313− side,
314− )
315−}
316−
317−func joinLetterform(letters ...string) string {
318− return lipgloss.JoinHorizontal(lipgloss.Top, letters...)
319−}
320−
321−// letterformProps defines letterform stretching properties.
322−// for readability.
323−type letterformProps struct {
324− width int
325− minStretch int
326− maxStretch int
327− stretch bool
328−}
329−
330−// stretchLetterformPart is a helper function for letter stretching. If randomize
331−// is false the minimum number will be used.
332−func stretchLetterformPart(s string, p letterformProps) string {
333− if p.maxStretch < p.minStretch {
334− p.minStretch, p.maxStretch = p.maxStretch, p.minStretch
335− }
336− n := p.width
337− if p.stretch {
338− n = cachedRandN(p.maxStretch-p.minStretch) + p.minStretch //nolint:gosec
339− }
340− parts := make([]string, n)
341− for i := range parts {
342− parts[i] = s
343− }
344− return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
345−}
internal/ui/logo/rand.go+2−2
@@ -14,8 +14,8 @@ func cachedRandN(n int) int {
1414 randCachesMu.Lock()
1515 defer randCachesMu.Unlock()
1616
17− if n, ok := randCaches[n]; ok {
18− return n
17+ if nPrime, ok := randCaches[n]; ok {
18+ return nPrime
1919 }
2020
2121 r := rand.IntN(n)
internal/ui/model/header.go+2−2
@@ -39,7 +39,7 @@ func newHeader(com *common.Common) *header {
3939 }
4040 t := com.Styles
4141 h.compactLogo = t.Header.Charm.Render("Charm™") + " " +
42− styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary) + " "
42+ styles.ApplyBoldForegroundGrad(t.Header.LogoGradCanvas, "CRUSH", t.Header.LogoGradFromColor, t.Header.LogoGradToColor) + " "
4343 return h
4444}
4545
@@ -102,7 +102,7 @@ func (h *header) drawHeader(
102102 b.WriteString(details)
103103
104104 view := uv.NewStyledString(
105− t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String()))
105+ t.Header.Wrapper.Padding(0, rightPadding, 0, leftPadding).Render(b.String()))
106106 view.Draw(scr, area)
107107}
108108
internal/ui/model/layout_test.go+1−1
@@ -30,7 +30,7 @@ func newTestUI() *UI {
3030 com := common.DefaultCommon(nil)
3131
3232 ta := textarea.New()
33− ta.SetStyles(com.Styles.TextArea)
33+ ta.SetStyles(com.Styles.Editor.Textarea)
3434 ta.ShowLineNumbers = false
3535 ta.CharLimit = -1
3636 ta.SetVirtualCursor(false)
internal/ui/model/lsp.go+16−16
@@ -41,11 +41,11 @@ func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
4141 lsps = append(lsps, LSPInfo{LSPClientInfo: state, Diagnostics: lspErrs})
4242 }
4343
44− title := t.ResourceGroupTitle.Render("LSPs")
44+ title := t.Resource.Heading.Render("LSPs")
4545 if isSection {
4646 title = common.Section(t, title, width)
4747 }
48− list := t.ResourceAdditionalText.Render("None")
48+ list := t.Resource.AdditionalText.Render("None")
4949 if len(lsps) > 0 {
5050 list = lspList(t, lsps, width, maxItems)
5151 }
@@ -80,31 +80,31 @@ func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string {
8080 var renderedLsps []string
8181 for _, l := range lsps {
8282 var icon string
83− title := t.ResourceName.Render(l.Name)
83+ title := t.Resource.Name.Render(l.Name)
8484 var description string
8585 var diagnostics string
8686 switch l.State {
8787 case lsp.StateUnstarted:
88− icon = t.ResourceOfflineIcon.String()
89− description = t.ResourceStatus.Render("unstarted")
88+ icon = t.Resource.OfflineIcon.String()
89+ description = t.Resource.StatusText.Render("unstarted")
9090 case lsp.StateStopped:
91− icon = t.ResourceOfflineIcon.String()
92− description = t.ResourceStatus.Render("stopped")
91+ icon = t.Resource.OfflineIcon.String()
92+ description = t.Resource.StatusText.Render("stopped")
9393 case lsp.StateStarting:
94− icon = t.ResourceBusyIcon.String()
95− description = t.ResourceStatus.Render("starting...")
94+ icon = t.Resource.BusyIcon.String()
95+ description = t.Resource.StatusText.Render("starting...")
9696 case lsp.StateReady:
97− icon = t.ResourceOnlineIcon.String()
97+ icon = t.Resource.OnlineIcon.String()
9898 diagnostics = lspDiagnostics(t, l.Diagnostics)
9999 case lsp.StateError:
100− icon = t.ResourceErrorIcon.String()
101− description = t.ResourceStatus.Render("error")
100+ icon = t.Resource.ErrorIcon.String()
101+ description = t.Resource.StatusText.Render("error")
102102 if l.Error != nil {
103− description = t.ResourceStatus.Render(fmt.Sprintf("error: %s", l.Error.Error()))
103+ description = t.Resource.StatusText.Render(fmt.Sprintf("error: %s", l.Error.Error()))
104104 }
105105 case lsp.StateDisabled:
106− icon = t.ResourceOfflineIcon.Foreground(t.Muted.GetBackground()).String()
107− description = t.ResourceStatus.Render("disabled")
106+ icon = t.Resource.DisabledIcon.String()
107+ description = t.Resource.StatusText.Render("disabled")
108108 default:
109109 continue
110110 }
@@ -119,7 +119,7 @@ func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string {
119119 if len(renderedLsps) > maxItems {
120120 visibleItems := renderedLsps[:maxItems-1]
121121 remaining := len(renderedLsps) - maxItems
122− visibleItems = append(visibleItems, t.ResourceAdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
122+ visibleItems = append(visibleItems, t.Resource.AdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
123123 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
124124 }
125125 return lipgloss.JoinVertical(lipgloss.Left, renderedLsps...)
internal/ui/model/mcp.go+16−16
@@ -23,11 +23,11 @@ func (m *UI) mcpInfo(width, maxItems int, isSection bool) string {
2323 }
2424 }
2525
26− title := t.ResourceGroupTitle.Render("MCPs")
26+ title := t.Resource.Heading.Render("MCPs")
2727 if isSection {
2828 title = common.Section(t, title, width)
2929 }
30− list := t.ResourceAdditionalText.Render("None")
30+ list := t.Resource.AdditionalText.Render("None")
3131 if len(mcps) > 0 {
3232 list = mcpList(t, mcps, width, maxItems)
3333 }
@@ -39,13 +39,13 @@ func (m *UI) mcpInfo(width, maxItems int, isSection bool) string {
3939func mcpCounts(t *styles.Styles, counts mcp.Counts) string {
4040 var parts []string
4141 if counts.Tools > 0 {
42− parts = append(parts, t.Subtle.Render(fmt.Sprintf("%d tools", counts.Tools)))
42+ parts = append(parts, t.Resource.CapabilityCount.Render(fmt.Sprintf("%d tools", counts.Tools)))
4343 }
4444 if counts.Prompts > 0 {
45− parts = append(parts, t.Subtle.Render(fmt.Sprintf("%d prompts", counts.Prompts)))
45+ parts = append(parts, t.Resource.CapabilityCount.Render(fmt.Sprintf("%d prompts", counts.Prompts)))
4646 }
4747 if counts.Resources > 0 {
48− parts = append(parts, t.Subtle.Render(fmt.Sprintf("%d resources", counts.Resources)))
48+ parts = append(parts, t.Resource.CapabilityCount.Render(fmt.Sprintf("%d resources", counts.Resources)))
4949 }
5050 return strings.Join(parts, " ")
5151}
@@ -65,28 +65,28 @@ func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) strin
6565 if m.Name == config.DockerMCPName {
6666 title = "Docker MCP"
6767 }
68− title = t.ResourceName.Render(title)
68+ title = t.Resource.Name.Render(title)
6969 var description string
7070 var extraContent string
7171
7272 switch m.State {
7373 case mcp.StateStarting:
74− icon = t.ResourceBusyIcon.String()
75− description = t.ResourceStatus.Render("starting...")
74+ icon = t.Resource.BusyIcon.String()
75+ description = t.Resource.StatusText.Render("starting...")
7676 case mcp.StateConnected:
77− icon = t.ResourceOnlineIcon.String()
77+ icon = t.Resource.OnlineIcon.String()
7878 extraContent = mcpCounts(t, m.Counts)
7979 case mcp.StateError:
80− icon = t.ResourceErrorIcon.String()
81− description = t.ResourceStatus.Render("error")
80+ icon = t.Resource.ErrorIcon.String()
81+ description = t.Resource.StatusText.Render("error")
8282 if m.Error != nil {
83− description = t.ResourceStatus.Render(fmt.Sprintf("error: %s", m.Error.Error()))
83+ description = t.Resource.StatusText.Render(fmt.Sprintf("error: %s", m.Error.Error()))
8484 }
8585 case mcp.StateDisabled:
86− icon = t.ResourceOfflineIcon.Foreground(t.Muted.GetBackground()).String()
87− description = t.ResourceStatus.Render("disabled")
86+ icon = t.Resource.DisabledIcon.String()
87+ description = t.Resource.StatusText.Render("disabled")
8888 default:
89− icon = t.ResourceOfflineIcon.String()
89+ icon = t.Resource.OfflineIcon.String()
9090 }
9191
9292 renderedMcps = append(renderedMcps, common.Status(t, common.StatusOpts{
@@ -100,7 +100,7 @@ func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) strin
100100 if len(renderedMcps) > maxItems {
101101 visibleItems := renderedMcps[:maxItems-1]
102102 remaining := len(renderedMcps) - maxItems
103− visibleItems = append(visibleItems, t.ResourceAdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
103+ visibleItems = append(visibleItems, t.Resource.AdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
104104 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
105105 }
106106 return lipgloss.JoinVertical(lipgloss.Left, renderedMcps...)
internal/ui/model/pills.go+6−6
@@ -56,12 +56,12 @@ func queuePill(queue int, focused, panelFocused bool, t *styles.Styles) string {
5656 if queue <= 0 {
5757 return ""
5858 }
59− triangles := styles.ForegroundGrad(t, "▶▶▶▶▶▶▶▶▶", false, t.RedDark, t.Secondary)
59+ triangles := styles.ForegroundGrad(t.Pills.QueueIconBase, "▶▶▶▶▶▶▶▶▶", false, t.Pills.QueueGradFromColor, t.Pills.QueueGradToColor)
6060 if queue < len(triangles) {
6161 triangles = triangles[:queue]
6262 }
6363
64− text := t.Base.Render(fmt.Sprintf("%d Queued", queue))
64+ text := t.Pills.QueueLabel.Render(fmt.Sprintf("%d Queued", queue))
6565 content := fmt.Sprintf("%s %s", strings.Join(triangles, ""), text)
6666 return pillStyle(focused, panelFocused, t).Render(content)
6767}
@@ -87,8 +87,8 @@ func todoPill(todos []session.Todo, spinnerView string, focused, panelFocused bo
8787
8888 total := len(todos)
8989
90− label := t.Base.Render("To-Do")
91− progress := t.Muted.Render(fmt.Sprintf("%d/%d", completed, total))
90+ label := t.Pills.TodoLabel.Render("To-Do")
91+ progress := t.Pills.TodoProgress.Render(fmt.Sprintf("%d/%d", completed, total))
9292
9393 var content string
9494 if panelFocused {
@@ -101,7 +101,7 @@ func todoPill(todos []session.Todo, spinnerView string, focused, panelFocused bo
101101 if len(taskText) > maxTaskDisplayLength {
102102 taskText = taskText[:maxTaskDisplayLength-1] + "…"
103103 }
104− task := t.Subtle.Render(taskText)
104+ task := t.Pills.TodoCurrentTask.Render(taskText)
105105 content = fmt.Sprintf("%s %s %s %s", spinnerView, label, progress, task)
106106 } else {
107107 content = fmt.Sprintf("%s %s", label, progress)
@@ -128,7 +128,7 @@ func queueList(queueItems []string, t *styles.Styles) string {
128128 text = text[:maxQueueDisplayLength-1] + "…"
129129 }
130130 prefix := t.Pills.QueueItemPrefix.Render() + " "
131− lines = append(lines, prefix+t.Muted.Render(text))
131+ lines = append(lines, prefix+t.Pills.QueueItemText.Render(text))
132132 }
133133
134134 return strings.Join(lines, "\n")
internal/ui/model/session.go+3−3
@@ -163,11 +163,11 @@ func (m *UI) handleFileEvent(file history.File) tea.Cmd {
163163func (m *UI) filesInfo(cwd string, width, maxItems int, isSection bool) string {
164164 t := m.com.Styles
165165
166− title := t.Subtle.Render("Modified Files")
166+ title := t.Files.SectionTitle.Render("Modified Files")
167167 if isSection {
168168 title = common.Section(t, "Modified Files", width)
169169 }
170− list := t.Subtle.Render("None")
170+ list := t.Files.EmptyMessage.Render("None")
171171 var filesWithChanges []SessionFile
172172 for _, f := range m.sessionFiles {
173173 if f.Additions == 0 && f.Deletions == 0 {
@@ -226,7 +226,7 @@ func fileList(t *styles.Styles, cwd string, filesWithChanges []SessionFile, widt
226226
227227 if len(filesWithChanges) > maxItems {
228228 remaining := len(filesWithChanges) - maxItems
229− renderedFiles = append(renderedFiles, t.Subtle.Render(fmt.Sprintf("…and %d more", remaining)))
229+ renderedFiles = append(renderedFiles, t.Files.TruncationHint.Render(fmt.Sprintf("…and %d more", remaining)))
230230 }
231231
232232 return lipgloss.JoinVertical(lipgloss.Left, renderedFiles...)
internal/ui/model/sidebar.go+1−1
@@ -138,7 +138,7 @@ func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) {
138138 width := area.Dx()
139139 height := area.Dy()
140140
141− title := t.Muted.Width(width).MaxHeight(2).Render(m.session.Title)
141+ title := t.Sidebar.SessionTitle.Width(width).MaxHeight(2).Render(m.session.Title)
142142 cwd := common.PrettyPath(t, m.com.Workspace.WorkingDir(), width)
143143 sidebarLogo := m.sidebarLogo
144144 if height < logoHeightBreakpoint {
internal/ui/model/skills.go+8−8
@@ -38,14 +38,14 @@ func cachedBuiltinSkills() []*skills.Skill {
3838func (m *UI) skillsInfo(width, maxItems int, isSection bool) string {
3939 t := m.com.Styles
4040
41− title := t.ResourceGroupTitle.Render("Skills")
41+ title := t.Resource.Heading.Render("Skills")
4242 if isSection {
4343 title = common.Section(t, title, width)
4444 }
4545
4646 items := m.skillStatusItems()
4747 if len(items) == 0 {
48− list := t.ResourceAdditionalText.Render("None")
48+ list := t.Resource.AdditionalText.Render("None")
4949 return lipgloss.NewStyle().Width(width).Render(fmt.Sprintf("%s\n\n%s", title, list))
5050 }
5151
@@ -68,14 +68,14 @@ func (m *UI) skillStatusItems() []skillStatusItem {
6868 name = filepath.Base(filepath.Dir(state.Path))
6969 }
7070 stateNames[name] = struct{}{}
71− icon := t.ResourceOnlineIcon.String()
71+ icon := t.Resource.OnlineIcon.String()
7272 if state.State == skills.StateError {
73− icon = t.ResourceErrorIcon.String()
73+ icon = t.Resource.ErrorIcon.String()
7474 }
7575 items = append(items, skillStatusItem{
7676 icon: icon,
7777 name: name,
78− title: t.ResourceName.Render(name),
78+ title: t.Resource.Name.Render(name),
7979 })
8080 }
8181
@@ -88,9 +88,9 @@ func (m *UI) skillStatusItems() []skillStatusItem {
8888 continue
8989 }
9090 items = append(items, skillStatusItem{
91− icon: t.ResourceOnlineIcon.String(),
91+ icon: t.Resource.OnlineIcon.String(),
9292 name: skill.Name,
93− title: t.ResourceName.Render(skill.Name),
93+ title: t.Resource.Name.Render(skill.Name),
9494 })
9595 }
9696
@@ -111,7 +111,7 @@ func skillsList(t *styles.Styles, items []skillStatusItem, width, maxItems int)
111111 remaining := len(items) - (maxItems - 1)
112112 items = append(visibleItems, skillStatusItem{
113113 name: "more",
114− title: t.ResourceAdditionalText.Render(fmt.Sprintf("…and %d more", remaining)),
114+ title: t.Resource.AdditionalText.Render(fmt.Sprintf("…and %d more", remaining)),
115115 })
116116 }
117117
internal/ui/model/skills_test.go+2−2
@@ -28,7 +28,7 @@ func TestSkillStatusItemsIncludesBuiltinSkills(t *testing.T) {
2828
2929 var hasGoDoc bool
3030 for _, item := range items {
31− if item.title == st.ResourceName.Render("go-doc") {
31+ if item.title == st.Resource.Name.Render("go-doc") {
3232 hasGoDoc = true
3333 break
3434 }
@@ -43,7 +43,7 @@ func TestSkillStatusItemsIncludesBuiltinSkills(t *testing.T) {
4343 if skill.Name == "go-doc" {
4444 continue
4545 }
46− expected := st.ResourceName.Render(skill.Name)
46+ expected := st.Resource.Name.Render(skill.Name)
4747 for _, item := range items {
4848 if item.title == expected {
4949 hasBuiltin = true
internal/ui/model/ui.go+14−14
@@ -269,7 +269,7 @@ type UI struct {
269269func New(com *common.Common, initialSessionID string, continueLast bool) *UI {
270270 // Editor components
271271 ta := textarea.New()
272− ta.SetStyles(com.Styles.TextArea)
272+ ta.SetStyles(com.Styles.Editor.Textarea)
273273 ta.ShowLineNumbers = false
274274 ta.CharLimit = -1
275275 ta.SetVirtualCursor(false)
@@ -2745,9 +2745,9 @@ func (m *UI) normalPromptFunc(info textarea.PromptInfo) string {
27452745 return "::: "
27462746 }
27472747 if info.Focused {
2748− return t.EditorPromptNormalFocused.Render()
2748+ return t.Editor.PromptNormalFocused.Render()
27492749 }
2750− return t.EditorPromptNormalBlurred.Render()
2750+ return t.Editor.PromptNormalBlurred.Render()
27512751}
27522752
27532753// yoloPromptFunc returns the yolo mode editor prompt style with warning icon
@@ -2756,15 +2756,15 @@ func (m *UI) yoloPromptFunc(info textarea.PromptInfo) string {
27562756 t := m.com.Styles
27572757 if info.LineNumber == 0 {
27582758 if info.Focused {
2759− return t.EditorPromptYoloIconFocused.Render()
2759+ return t.Editor.PromptYoloIconFocused.Render()
27602760 } else {
2761− return t.EditorPromptYoloIconBlurred.Render()
2761+ return t.Editor.PromptYoloIconBlurred.Render()
27622762 }
27632763 }
27642764 if info.Focused {
2765− return t.EditorPromptYoloDotsFocused.Render()
2765+ return t.Editor.PromptYoloDotsFocused.Render()
27662766 }
2767− return t.EditorPromptYoloDotsBlurred.Render()
2767+ return t.Editor.PromptYoloDotsBlurred.Render()
27682768}
27692769
27702770// closeCompletions closes the completions popup and resets state.
@@ -3536,7 +3536,7 @@ func (m *UI) drawSessionDetails(scr uv.Screen, area uv.Rectangle) {
35363536 blocks...,
35373537 )
35383538
3539− version := s.CompactDetails.Version.Foreground(s.Border).Width(width).AlignHorizontal(lipgloss.Right).Render(version.Version)
3539+ version := s.CompactDetails.Version.Width(width).AlignHorizontal(lipgloss.Right).Render(version.Version)
35403540
35413541 remainingHeight := height - lipgloss.Height(detailsHeader) - lipgloss.Height(version)
35423542
@@ -3651,12 +3651,12 @@ func (m *UI) disableDockerMCP() tea.Msg {
36513651
36523652// renderLogo renders the Crush logo with the given styles and dimensions.
36533653func renderLogo(t *styles.Styles, compact bool, width int) string {
3654− return logo.Render(t, version.Version, compact, logo.Opts{
3655− FieldColor: t.LogoFieldColor,
3656− TitleColorA: t.LogoTitleColorA,
3657− TitleColorB: t.LogoTitleColorB,
3658− CharmColor: t.LogoCharmColor,
3659− VersionColor: t.LogoVersionColor,
3654+ return logo.Render(t.Logo.GradCanvas, version.Version, compact, logo.Opts{
3655+ FieldColor: t.Logo.FieldColor,
3656+ TitleColorA: t.Logo.TitleColorA,
3657+ TitleColorB: t.Logo.TitleColorB,
3658+ CharmColor: t.Logo.CharmColor,
3659+ VersionColor: t.Logo.VersionColor,
36603660 Width: width,
36613661 })
36623662}
internal/ui/styles/grad.go+9−56
@@ -5,7 +5,7 @@ import (
55 "image/color"
66 "strings"
77
8− "github.com/lucasb-eyer/go-colorful"
8+ "charm.land/lipgloss/v2"
99 "github.com/rivo/uniseg"
1010)
1111
@@ -13,12 +13,12 @@ import (
1313// rendered with a horizontal gradient foreground from color1 to color2. Each
1414// string in the returned slice corresponds to a grapheme cluster in the input
1515// string. If bold is true, the rendered strings will be bolded.
16−func ForegroundGrad(t *Styles, input string, bold bool, color1, color2 color.Color) []string {
16+func ForegroundGrad(base lipgloss.Style, input string, bold bool, color1, color2 color.Color) []string {
1717 if input == "" {
1818 return []string{""}
1919 }
2020 if len(input) == 1 {
21− style := t.Base.Foreground(color1)
21+ style := base.Foreground(color1)
2222 if bold {
2323 style.Bold(true)
2424 }
@@ -30,9 +30,9 @@ func ForegroundGrad(t *Styles, input string, bold bool, color1, color2 color.Col
3030 clusters = append(clusters, string(gr.Runes()))
3131 }
3232
33− ramp := blendColors(len(clusters), color1, color2)
33+ ramp := lipgloss.Blend1D(len(clusters), color1, color2)
3434 for i, c := range ramp {
35− style := t.Base.Foreground(c)
35+ style := base.Foreground(c)
3636 if bold {
3737 style.Bold(true)
3838 }
@@ -43,12 +43,12 @@ func ForegroundGrad(t *Styles, input string, bold bool, color1, color2 color.Col
4343
4444// ApplyForegroundGrad renders a given string with a horizontal gradient
4545// foreground.
46−func ApplyForegroundGrad(t *Styles, input string, color1, color2 color.Color) string {
46+func ApplyForegroundGrad(base lipgloss.Style, input string, color1, color2 color.Color) string {
4747 if input == "" {
4848 return ""
4949 }
5050 var o strings.Builder
51− clusters := ForegroundGrad(t, input, false, color1, color2)
51+ clusters := ForegroundGrad(base, input, false, color1, color2)
5252 for _, c := range clusters {
5353 fmt.Fprint(&o, c)
5454 }
@@ -57,61 +57,14 @@ func ApplyForegroundGrad(t *Styles, input string, color1, color2 color.Color) st
5757
5858// ApplyBoldForegroundGrad renders a given string with a horizontal gradient
5959// foreground.
60−func ApplyBoldForegroundGrad(t *Styles, input string, color1, color2 color.Color) string {
60+func ApplyBoldForegroundGrad(base lipgloss.Style, input string, color1, color2 color.Color) string {
6161 if input == "" {
6262 return ""
6363 }
6464 var o strings.Builder
65− clusters := ForegroundGrad(t, input, true, color1, color2)
65+ clusters := ForegroundGrad(base, input, true, color1, color2)
6666 for _, c := range clusters {
6767 fmt.Fprint(&o, c)
6868 }
6969 return o.String()
7070}
71−
72−// blendColors returns a slice of colors blended between the given keys.
73−// Blending is done in Hcl to stay in gamut.
74−func blendColors(size int, stops ...color.Color) []color.Color {
75− if len(stops) < 2 {
76− return nil
77− }
78−
79− stopsPrime := make([]colorful.Color, len(stops))
80− for i, k := range stops {
81− stopsPrime[i], _ = colorful.MakeColor(k)
82− }
83−
84− numSegments := len(stopsPrime) - 1
85− blended := make([]color.Color, 0, size)
86−
87− // Calculate how many colors each segment should have.
88− segmentSizes := make([]int, numSegments)
89− baseSize := size / numSegments
90− remainder := size % numSegments
91−
92− // Distribute the remainder across segments.
93− for i := range numSegments {
94− segmentSizes[i] = baseSize
95− if i < remainder {
96− segmentSizes[i]++
97− }
98− }
99−
100− // Generate colors for each segment.
101− for i := range numSegments {
102− c1 := stopsPrime[i]
103− c2 := stopsPrime[i+1]
104− segmentSize := segmentSizes[i]
105−
106− for j := range segmentSize {
107− var t float64
108− if segmentSize > 1 {
109− t = float64(j) / float64(segmentSize-1)
110− }
111− c := c1.BlendHcl(c2, t)
112− blended = append(blended, c)
113− }
114− }
115−
116− return blended
117−}
internal/ui/styles/styles.go+387−295
@@ -58,28 +58,19 @@ const (
5858)
5959
6060type Styles struct {
61− WindowTooSmall lipgloss.Style
62−
63− // Reusable text styles
64− Base lipgloss.Style
65− Muted lipgloss.Style
66− HalfMuted lipgloss.Style
67− Subtle lipgloss.Style
68−
69− // Tags
70− TagBase lipgloss.Style
71− TagError lipgloss.Style
72− TagInfo lipgloss.Style
73−
7461 // Header
7562 Header struct {
76− Charm lipgloss.Style // Style for "Charm™" label
77− Diagonals lipgloss.Style // Style for diagonal separators (╱)
78− Percentage lipgloss.Style // Style for context percentage
79− Keystroke lipgloss.Style // Style for keystroke hints (e.g., "ctrl+d")
80− KeystrokeTip lipgloss.Style // Style for keystroke action text (e.g., "open", "close")
81− WorkingDir lipgloss.Style // Style for current working directory
82− Separator lipgloss.Style // Style for separator dots (•)
63+ Charm lipgloss.Style // Style for "Charm™" label
64+ Diagonals lipgloss.Style // Style for diagonal separators (╱)
65+ Percentage lipgloss.Style // Style for context percentage
66+ Keystroke lipgloss.Style // Style for keystroke hints (e.g., "ctrl+d")
67+ KeystrokeTip lipgloss.Style // Style for keystroke action text (e.g., "open", "close")
68+ WorkingDir lipgloss.Style // Style for current working directory
69+ Separator lipgloss.Style // Style for separator dots (•)
70+ Wrapper lipgloss.Style // Outer container for the entire header row
71+ LogoGradCanvas lipgloss.Style // Canvas for the compact "CRUSH" gradient
72+ LogoGradFromColor color.Color // "CRUSH" wordmark gradient start
73+ LogoGradToColor color.Color // "CRUSH" wordmark gradient end
8374 }
8475
8576 CompactDetails struct {
@@ -88,43 +79,18 @@ type Styles struct {
8879 Title lipgloss.Style
8980 }
9081
91− // Panels
92− PanelMuted lipgloss.Style
93− PanelBase lipgloss.Style
94−
95− // Line numbers for code blocks
96− LineNumber lipgloss.Style
97−
98− // Message borders
99− FocusedMessageBorder lipgloss.Border
100−
10182 // Tool calls
102− ToolCallPending lipgloss.Style
103− ToolCallError lipgloss.Style
104− ToolCallSuccess lipgloss.Style
105− ToolCallCancelled lipgloss.Style
106− EarlyStateMessage lipgloss.Style
83+ ToolCallSuccess lipgloss.Style
10784
10885 // Text selection
10986 TextSelection lipgloss.Style
11087
111− // LSP and MCP status indicators
112− ResourceGroupTitle lipgloss.Style
113− ResourceOfflineIcon lipgloss.Style
114− ResourceBusyIcon lipgloss.Style
115− ResourceErrorIcon lipgloss.Style
116− ResourceOnlineIcon lipgloss.Style
117− ResourceName lipgloss.Style
118− ResourceStatus lipgloss.Style
119− ResourceAdditionalText lipgloss.Style
120−
12188 // Markdown & Chroma
12289 Markdown ansi.StyleConfig
123− PlainMarkdown ansi.StyleConfig
90+ QuietMarkdown ansi.StyleConfig
12491
12592 // Inputs
12693 TextInput textinput.Styles
127− TextArea textarea.Styles
12894
12995 // Help
13096 Help help.Styles
@@ -136,62 +102,55 @@ type Styles struct {
136102 FilePicker filepicker.Styles
137103
138104 // Buttons
139− ButtonFocus lipgloss.Style
140− ButtonBlur lipgloss.Style
141−
142− // Borders
143− BorderFocus lipgloss.Style
144− BorderBlur lipgloss.Style
105+ Button struct {
106+ Focused lipgloss.Style
107+ Blurred lipgloss.Style
108+ }
145109
146110 // Editor
147− EditorPromptNormalFocused lipgloss.Style
148− EditorPromptNormalBlurred lipgloss.Style
149− EditorPromptYoloIconFocused lipgloss.Style
150− EditorPromptYoloIconBlurred lipgloss.Style
151− EditorPromptYoloDotsFocused lipgloss.Style
152− EditorPromptYoloDotsBlurred lipgloss.Style
111+ Editor struct {
112+ Textarea textarea.Styles
113+
114+ // Normal mode prompt (default "::: ").
115+ PromptNormalFocused lipgloss.Style
116+ PromptNormalBlurred lipgloss.Style
117+
118+ // YOLO mode prompt (" ! " icon + ":::" dots).
119+ PromptYoloIconFocused lipgloss.Style
120+ PromptYoloIconBlurred lipgloss.Style
121+ PromptYoloDotsFocused lipgloss.Style
122+ PromptYoloDotsBlurred lipgloss.Style
123+ }
153124
154125 // Radio
155− RadioOn lipgloss.Style
156− RadioOff lipgloss.Style
126+ Radio struct {
127+ On lipgloss.Style
128+ Off lipgloss.Style
129+ Label lipgloss.Style // Text next to a radio button
130+ }
157131
158132 // Background
159133 Background color.Color
160134
161135 // Logo
162− LogoFieldColor color.Color
163− LogoTitleColorA color.Color
164− LogoTitleColorB color.Color
165− LogoCharmColor color.Color
166− LogoVersionColor color.Color
167−
168− // Colors - semantic colors for tool rendering.
169− Primary color.Color
170− Secondary color.Color
171− Tertiary color.Color
172− BgBase color.Color
173− BgBaseLighter color.Color
174− BgSubtle color.Color
175− BgOverlay color.Color
176− FgBase color.Color
177− FgMuted color.Color
178− FgHalfMuted color.Color
179− FgSubtle color.Color
180− Border color.Color
181− BorderColor color.Color // Border focus color
182− Error color.Color
183− Warning color.Color
184− Info color.Color
185− White color.Color
186− BlueLight color.Color
187− Blue color.Color
188− BlueDark color.Color
189− GreenLight color.Color
190− Green color.Color
191− GreenDark color.Color
192− Red color.Color
193− RedDark color.Color
194− Yellow color.Color
136+ Logo struct {
137+ FieldColor color.Color
138+ TitleColorA color.Color
139+ TitleColorB color.Color
140+ CharmColor color.Color
141+ VersionColor color.Color
142+ SmallCharm lipgloss.Style // "Charm™" label in SmallRender
143+ SmallDiagonals lipgloss.Style // Diagonal line fill in SmallRender
144+ GradCanvas lipgloss.Style // Blank canvas for gradient painting
145+ SmallGradFromColor color.Color // Small "Crush" wordmark gradient start
146+ SmallGradToColor color.Color // Small "Crush" wordmark gradient end
147+ }
148+
149+ // Working indicator gradient (spinners/shimmers on assistant "thinking",
150+ // tool-call pending, CLI generating, startup).
151+ WorkingGradFromColor color.Color
152+ WorkingGradToColor color.Color
153+ WorkingLabelColor color.Color // Label text color next to the indicator
195154
196155 // Section Title
197156 Section struct {
@@ -214,41 +173,80 @@ type Styles struct {
214173 InfoDiagnostic lipgloss.Style
215174 }
216175
176+ // Sidebar
177+ Sidebar struct {
178+ SessionTitle lipgloss.Style // Current session title at top of sidebar
179+ WorkingDir lipgloss.Style // Working directory path (PrettyPath)
180+ }
181+
182+ // ModelInfo (model name, provider, reasoning, token/cost summary)
183+ ModelInfo struct {
184+ Icon lipgloss.Style // Model icon (◇)
185+ Name lipgloss.Style // Model name text
186+ Provider lipgloss.Style // "via <provider>" text
187+ ProviderFallback lipgloss.Style // Provider on its own second line
188+ Reasoning lipgloss.Style // Reasoning effort text
189+ TokenCount lipgloss.Style // "(42K)" token count
190+ TokenPercentage lipgloss.Style // "42%" percent of context window
191+ Cost lipgloss.Style // "$0.42" cost readout
192+ }
193+
194+ // Resource styles the LSP/MCP/skills sidebar lists: their heading,
195+ // each row's status icon, name, status text, and truncation hints.
196+ Resource struct {
197+ Heading lipgloss.Style // Section header ("LSPs", "MCPs", "Skills")
198+ Name lipgloss.Style // Resource name (e.g. "gopls")
199+ StatusText lipgloss.Style // Row status description (e.g. "starting...")
200+ OfflineIcon lipgloss.Style // Offline/unstarted/stopped status icon
201+ DisabledIcon lipgloss.Style // Disabled status icon
202+ BusyIcon lipgloss.Style // Busy/starting status icon
203+ ErrorIcon lipgloss.Style // Error status icon
204+ OnlineIcon lipgloss.Style // Online/ready status icon
205+ AdditionalText lipgloss.Style // "None" and "…and N more" text
206+ CapabilityCount lipgloss.Style // "N tools" / "N prompts" / "N resources"
207+ RowTitleBase lipgloss.Style // Base style applied over row titles in common.Status
208+ RowDescBase lipgloss.Style // Base style applied over row descriptions in common.Status
209+ DefaultTitleFg color.Color // Default title color when opt is zero
210+ DefaultDescFg color.Color // Default description color when opt is zero
211+ }
212+
217213 // Files
218214 Files struct {
219− Path lipgloss.Style
220− Additions lipgloss.Style
221− Deletions lipgloss.Style
215+ Path lipgloss.Style
216+ Additions lipgloss.Style
217+ Deletions lipgloss.Style
218+ SectionTitle lipgloss.Style // "Modified Files" heading
219+ EmptyMessage lipgloss.Style // "None" placeholder when no files
220+ TruncationHint lipgloss.Style // "…and N more" message
222221 }
223222
224223 // Chat
225− Chat struct {
226− // Message item styles
227− Message struct {
228− UserBlurred lipgloss.Style
229− UserFocused lipgloss.Style
230− AssistantBlurred lipgloss.Style
231− AssistantFocused lipgloss.Style
232− NoContent lipgloss.Style
233− Thinking lipgloss.Style
234− ErrorTag lipgloss.Style
235− ErrorTitle lipgloss.Style
236− ErrorDetails lipgloss.Style
237− ToolCallFocused lipgloss.Style
238− ToolCallCompact lipgloss.Style
239− ToolCallBlurred lipgloss.Style
240− SectionHeader lipgloss.Style
241−
242− // Thinking section styles
243− ThinkingBox lipgloss.Style // Background for thinking content
244− ThinkingTruncationHint lipgloss.Style // "… (N lines hidden)" hint
245− ThinkingFooterTitle lipgloss.Style // "Thought for" text
246− ThinkingFooterDuration lipgloss.Style // Duration value
247− AssistantInfoIcon lipgloss.Style
248− AssistantInfoModel lipgloss.Style
249− AssistantInfoProvider lipgloss.Style
250− AssistantInfoDuration lipgloss.Style
251− }
224+ // Messages - chat message item styles
225+ Messages struct {
226+ UserBlurred lipgloss.Style
227+ UserFocused lipgloss.Style
228+ AssistantBlurred lipgloss.Style
229+ AssistantFocused lipgloss.Style
230+ NoContent lipgloss.Style
231+ Thinking lipgloss.Style
232+ ErrorTag lipgloss.Style
233+ ErrorTitle lipgloss.Style
234+ ErrorDetails lipgloss.Style
235+ ToolCallFocused lipgloss.Style
236+ ToolCallCompact lipgloss.Style
237+ ToolCallBlurred lipgloss.Style
238+ SectionHeader lipgloss.Style
239+
240+ // Thinking section styles
241+ ThinkingBox lipgloss.Style // Background for thinking content
242+ ThinkingTruncationHint lipgloss.Style // "… (N lines hidden)" hint
243+ ThinkingFooterTitle lipgloss.Style // "Thought for" text
244+ ThinkingFooterDuration lipgloss.Style // Duration value
245+ AssistantInfoIcon lipgloss.Style
246+ AssistantInfoModel lipgloss.Style
247+ AssistantInfoProvider lipgloss.Style
248+ AssistantInfoDuration lipgloss.Style
249+ AssistantCanceled lipgloss.Style // Italic "Canceled" footer
252250 }
253251
254252 // Tool - styles for tool call rendering
@@ -316,6 +314,9 @@ type Styles struct {
316314 TodoCompletedIcon lipgloss.Style // Completed todo icon
317315 TodoInProgressIcon lipgloss.Style // In-progress todo icon
318316 TodoPendingIcon lipgloss.Style // Pending todo icon
317+ TodoStatusNote lipgloss.Style // " · completed N" / " · starting task" trailing note
318+ TodoItem lipgloss.Style // Default body text for todo list items
319+ TodoJustStarted lipgloss.Style // Text of the just-started todo in tool-call bodies
319320
320321 // MCP tools
321322 MCPName lipgloss.Style // The mcp name
@@ -329,17 +330,26 @@ type Styles struct {
329330 ResourceSize lipgloss.Style
330331 MediaType lipgloss.Style
331332
332− // Docker MCP tools
333− DockerMCPActionAdd lipgloss.Style // Docker MCP add action (green)
334− DockerMCPActionDel lipgloss.Style // Docker MCP remove action (red)
333+ // Action verb colors for tool-call headers.
334+ ActionCreate lipgloss.Style // Constructive actions (e.g. "Add", "Create")
335+ ActionDestroy lipgloss.Style // Destructive actions (e.g. "Remove", "Delete")
336+
337+ // Tool result helpers.
338+ ResultEmpty lipgloss.Style // "No results" placeholder
339+ ResultTruncation lipgloss.Style // "… and N more" truncation line
340+ ResultItemName lipgloss.Style // Item name (left column in result lists)
341+ ResultItemDesc lipgloss.Style // Item description (right column)
335342 }
336343
337344 // Dialog styles
338345 Dialog struct {
339− Title lipgloss.Style
340− TitleText lipgloss.Style
341− TitleError lipgloss.Style
342− TitleAccent lipgloss.Style
346+ Title lipgloss.Style
347+ TitleText lipgloss.Style
348+ TitleError lipgloss.Style
349+ TitleAccent lipgloss.Style
350+ TitleLineBase lipgloss.Style // Base for the gradient ╱╱╱ next to dialog titles
351+ TitleGradFromColor color.Color // Default dialog title ╱╱╱ gradient start
352+ TitleGradToColor color.Color // Default dialog title ╱╱╱ gradient end
343353 // View is the main content area style.
344354 View lipgloss.Style
345355 PrimaryText lipgloss.Style
@@ -381,7 +391,43 @@ type Styles struct {
381391 InputRequiredMarkFocused lipgloss.Style
382392 }
383393
384− Commands struct{}
394+ // ListItem styles the info-text rendered alongside list items (commands,
395+ // models, reasoning options). Sessions have their own overrides below.
396+ ListItem struct {
397+ InfoBlurred lipgloss.Style
398+ InfoFocused lipgloss.Style
399+ }
400+
401+ Models struct {
402+ ConfiguredText lipgloss.Style // "Configured" badge shown on the ModelGroup header
403+ }
404+
405+ Permissions struct {
406+ KeyText lipgloss.Style // Left key cell of a key/value row
407+ ValueText lipgloss.Style // Right value cell of a key/value row
408+ ParamsBg color.Color // Background color behind highlighted JSON parameters
409+ }
410+
411+ Quit struct {
412+ Content lipgloss.Style // Wrapper for the quit dialog's inner content
413+ Frame lipgloss.Style // Outer rounded border framing the quit dialog
414+ }
415+
416+ APIKey struct {
417+ Spinner lipgloss.Style // Loading spinner while validating the key
418+ }
419+
420+ OAuth struct {
421+ Spinner lipgloss.Style // Loading spinner
422+ Instructions lipgloss.Style // Emphasized instruction text
423+ UserCode lipgloss.Style // Prominent user code display
424+ Success lipgloss.Style // Positive status text (e.g. "Authentication successful!")
425+ Link lipgloss.Style // Underlined verification URL
426+ Enter lipgloss.Style // "enter" keyword highlight in instructions
427+ ErrorText lipgloss.Style // Error message when authentication fails
428+ StatusText lipgloss.Style // Narrative status text ("Initializing...", "Verifying...", etc.)
429+ UserCodeBg color.Color // Background color of the centered user-code box
430+ }
385431
386432 ImagePreview lipgloss.Style
387433
@@ -404,6 +450,9 @@ type Styles struct {
404450 RenamingTitleGradientFromColor color.Color
405451 RenamingTitleGradientToColor color.Color
406452 RenamingPlaceholder lipgloss.Style
453+
454+ InfoBlurred lipgloss.Style // Timestamp text on unfocused session items
455+ InfoFocused lipgloss.Style // Timestamp text on the focused session item
407456 }
408457 }
409458
@@ -441,14 +490,22 @@ type Styles struct {
441490
442491 // Pills styles for todo/queue pills
443492 Pills struct {
444− Base lipgloss.Style // Base pill style with padding
445− Focused lipgloss.Style // Focused pill with visible border
446− Blurred lipgloss.Style // Blurred pill with hidden border
447− QueueItemPrefix lipgloss.Style // Prefix for queue list items
448− HelpKey lipgloss.Style // Keystroke hint style
449− HelpText lipgloss.Style // Help action text style
450− Area lipgloss.Style // Pills area container
451− TodoSpinner lipgloss.Style // Todo spinner style
493+ Base lipgloss.Style // Base pill style with padding
494+ Focused lipgloss.Style // Focused pill with visible border
495+ Blurred lipgloss.Style // Blurred pill with hidden border
496+ QueueItemPrefix lipgloss.Style // Prefix for queue list items
497+ QueueItemText lipgloss.Style // Queue list item body text
498+ QueueLabel lipgloss.Style // "N Queued" label text
499+ QueueIconBase lipgloss.Style // Base style for queue gradient triangles
500+ QueueGradFromColor color.Color // Start color for queue indicator gradient
501+ QueueGradToColor color.Color // End color for queue indicator gradient
502+ TodoLabel lipgloss.Style // "To-Do" label
503+ TodoProgress lipgloss.Style // Todo ratio (e.g. "2/5")
504+ TodoCurrentTask lipgloss.Style // Current in-progress task name
505+ TodoSpinner lipgloss.Style // Todo spinner style
506+ HelpKey lipgloss.Style // Keystroke hint style
507+ HelpText lipgloss.Style // Help action text style
508+ Area lipgloss.Style // Pills area container
452509 }
453510}
454511
@@ -552,38 +609,17 @@ func DefaultStyles() Styles {
552609 normalBorder := lipgloss.NormalBorder()
553610
554611 base := lipgloss.NewStyle().Foreground(fgBase)
612+ muted := lipgloss.NewStyle().Foreground(fgMuted)
613+ subtle := lipgloss.NewStyle().Foreground(fgSubtle)
555614
556615 s := Styles{}
557616
558617 s.Background = bgBase
559618
560619 // Populate color fields
561− s.Primary = primary
562− s.Secondary = secondary
563− s.Tertiary = tertiary
564− s.BgBase = bgBase
565− s.BgBaseLighter = bgBaseLighter
566− s.BgSubtle = bgSubtle
567− s.BgOverlay = bgOverlay
568− s.FgBase = fgBase
569− s.FgMuted = fgMuted
570− s.FgHalfMuted = fgHalfMuted
571− s.FgSubtle = fgSubtle
572− s.Border = border
573− s.BorderColor = borderFocus
574− s.Error = error
575− s.Warning = warning
576− s.Info = info
577− s.White = white
578− s.BlueLight = blueLight
579− s.Blue = blue
580− s.BlueDark = blueDark
581− s.GreenLight = greenLight
582− s.Green = green
583− s.GreenDark = greenDark
584− s.Red = red
585− s.RedDark = redDark
586− s.Yellow = yellow
620+ s.WorkingGradFromColor = primary
621+ s.WorkingGradToColor = secondary
622+ s.WorkingLabelColor = fgBase
587623
588624 s.TextInput = textinput.Styles{
589625 Focused: textinput.StyleState{
@@ -605,7 +641,7 @@ func DefaultStyles() Styles {
605641 },
606642 }
607643
608− s.TextArea = textarea.Styles{
644+ s.Editor.Textarea = textarea.Styles{
609645 Focused: textarea.StyleState{
610646 Base: base,
611647 Text: base,
@@ -840,10 +876,10 @@ func DefaultStyles() Styles {
840876 },
841877 }
842878
843− // PlainMarkdown style - muted colors on subtle background for thinking content.
879+ // QuietMarkdown style - muted colors on subtle background for thinking content.
844880 plainBg := new(bgBaseLighter.Hex())
845881 plainFg := new(fgMuted.Hex())
846− s.PlainMarkdown = ansi.StyleConfig{
882+ s.QuietMarkdown = ansi.StyleConfig{
847883 Document: ansi.StyleBlock{
848884 StylePrimitive: ansi.StylePrimitive{
849885 Color: plainFg,
@@ -1081,72 +1117,47 @@ func DefaultStyles() Styles {
10811117 }
10821118
10831119 // borders
1084− s.FocusedMessageBorder = lipgloss.Border{Left: BorderThick}
1085−
1086− // text presets
1087− s.Base = lipgloss.NewStyle().Foreground(fgBase)
1088− s.Muted = lipgloss.NewStyle().Foreground(fgMuted)
1089− s.HalfMuted = lipgloss.NewStyle().Foreground(fgHalfMuted)
1090− s.Subtle = lipgloss.NewStyle().Foreground(fgSubtle)
1091−
1092− s.WindowTooSmall = s.Muted
1093−
1094− // tag presets
1095− s.TagBase = lipgloss.NewStyle().Padding(0, 1).Foreground(white)
1096− s.TagError = s.TagBase.Background(redDark)
1097− s.TagInfo = s.TagBase.Background(blueLight)
1120+ s.ToolCallSuccess = lipgloss.NewStyle().Foreground(green).SetString(ToolSuccess)
10981121
1099− // Compact header styles
11001122 s.Header.Charm = base.Foreground(secondary)
11011123 s.Header.Diagonals = base.Foreground(primary)
1102− s.Header.Percentage = s.Muted
1103− s.Header.Keystroke = s.Muted
1104− s.Header.KeystrokeTip = s.Subtle
1105− s.Header.WorkingDir = s.Muted
1106− s.Header.Separator = s.Subtle
1107−
1108− s.CompactDetails.Title = s.Base
1109− s.CompactDetails.View = s.Base.Padding(0, 1, 1, 1).Border(lipgloss.RoundedBorder()).BorderForeground(borderFocus)
1110− s.CompactDetails.Version = s.Muted
1111−
1112− // panels
1113− s.PanelMuted = s.Muted.Background(bgBaseLighter)
1114− s.PanelBase = lipgloss.NewStyle().Background(bgBase)
1115−
1116− // code line number
1117− s.LineNumber = lipgloss.NewStyle().Foreground(fgMuted).Background(bgBase).PaddingRight(1).PaddingLeft(1)
1118−
1119− // Tool calls
1120− s.ToolCallPending = lipgloss.NewStyle().Foreground(greenDark).SetString(ToolPending)
1121− s.ToolCallError = lipgloss.NewStyle().Foreground(redDark).SetString(ToolError)
1122− s.ToolCallSuccess = lipgloss.NewStyle().Foreground(green).SetString(ToolSuccess)
1123− // Cancelled uses muted tone but same glyph as pending
1124− s.ToolCallCancelled = s.Muted.SetString(ToolPending)
1125− s.EarlyStateMessage = s.Subtle.PaddingLeft(2)
1124+ s.Header.Percentage = muted
1125+ s.Header.Keystroke = muted
1126+ s.Header.KeystrokeTip = subtle
1127+ s.Header.WorkingDir = muted
1128+ s.Header.Separator = subtle
1129+ s.Header.Wrapper = lipgloss.NewStyle().Foreground(fgBase)
1130+ s.Header.LogoGradCanvas = lipgloss.NewStyle()
1131+ s.Header.LogoGradFromColor = secondary
1132+ s.Header.LogoGradToColor = primary
1133+
1134+ s.CompactDetails.Title = base
1135+ s.CompactDetails.View = base.Padding(0, 1, 1, 1).Border(lipgloss.RoundedBorder()).BorderForeground(borderFocus)
1136+ s.CompactDetails.Version = lipgloss.NewStyle().Foreground(border)
11261137
11271138 // Tool rendering styles
11281139 s.Tool.IconPending = base.Foreground(greenDark).SetString(ToolPending)
11291140 s.Tool.IconSuccess = base.Foreground(green).SetString(ToolSuccess)
11301141 s.Tool.IconError = base.Foreground(redDark).SetString(ToolError)
1131− s.Tool.IconCancelled = s.Muted.SetString(ToolPending)
1142+ s.Tool.IconCancelled = muted.SetString(ToolPending)
11321143
11331144 s.Tool.NameNormal = base.Foreground(blue)
11341145 s.Tool.NameNested = base.Foreground(blue)
11351146
1136− s.Tool.ParamMain = s.Subtle
1137− s.Tool.ParamKey = s.Subtle
1147+ s.Tool.ParamMain = subtle
1148+ s.Tool.ParamKey = subtle
11381149
11391150 // Content rendering - prepared styles that accept width parameter
1140− s.Tool.ContentLine = s.Muted.Background(bgBaseLighter)
1141− s.Tool.ContentTruncation = s.Muted.Background(bgBaseLighter)
1142− s.Tool.ContentCodeLine = s.Base.Background(bgBase).PaddingLeft(2)
1143− s.Tool.ContentCodeTruncation = s.Muted.Background(bgBase).PaddingLeft(2)
1151+ s.Tool.ContentLine = muted.Background(bgBaseLighter)
1152+ s.Tool.ContentTruncation = muted.Background(bgBaseLighter)
1153+ s.Tool.ContentCodeLine = base.Background(bgBase).PaddingLeft(2)
1154+ s.Tool.ContentCodeTruncation = muted.Background(bgBase).PaddingLeft(2)
11441155 s.Tool.ContentCodeBg = bgBase
11451156 s.Tool.Body = base.PaddingLeft(2)
11461157
11471158 // Deprecated - kept for backward compatibility
1148− s.Tool.ContentBg = s.Muted.Background(bgBaseLighter)
1149− s.Tool.ContentText = s.Muted
1159+ s.Tool.ContentBg = muted.Background(bgBaseLighter)
1160+ s.Tool.ContentText = muted
11501161 s.Tool.ContentLineNumber = base.Foreground(fgMuted).Background(bgBase).PaddingRight(1).PaddingLeft(1)
11511162
11521163 s.Tool.StateWaiting = base.Foreground(fgSubtle)
@@ -1156,7 +1167,7 @@ func DefaultStyles() Styles {
11561167 s.Tool.ErrorMessage = base.Foreground(fgHalfMuted)
11571168
11581169 // Diff and multi-edit styles
1159− s.Tool.DiffTruncation = s.Muted.Background(bgBaseLighter).PaddingLeft(2)
1170+ s.Tool.DiffTruncation = muted.Background(bgBaseLighter).PaddingLeft(2)
11601171 s.Tool.NoteTag = base.Padding(0, 1).Background(info).Foreground(white)
11611172 s.Tool.NoteMessage = base.Foreground(fgHalfMuted)
11621173
@@ -1166,12 +1177,12 @@ func DefaultStyles() Styles {
11661177 s.Tool.JobIconSuccess = base.Foreground(green)
11671178 s.Tool.JobToolName = base.Foreground(blue)
11681179 s.Tool.JobAction = base.Foreground(blueDark)
1169− s.Tool.JobPID = s.Muted
1170− s.Tool.JobDescription = s.Subtle
1180+ s.Tool.JobPID = muted
1181+ s.Tool.JobDescription = subtle
11711182
11721183 // Agent task styles
11731184 s.Tool.AgentTaskTag = base.Bold(true).Padding(0, 1).MarginLeft(2).Background(blueLight).Foreground(white)
1174− s.Tool.AgentPrompt = s.Muted
1185+ s.Tool.AgentPrompt = muted
11751186
11761187 // Agentic fetch styles
11771188 s.Tool.AgenticFetchPromptTag = base.Bold(true).Padding(0, 1).MarginLeft(2).Background(green).Foreground(border)
@@ -1181,6 +1192,9 @@ func DefaultStyles() Styles {
11811192 s.Tool.TodoCompletedIcon = base.Foreground(green)
11821193 s.Tool.TodoInProgressIcon = base.Foreground(greenDark)
11831194 s.Tool.TodoPendingIcon = base.Foreground(fgMuted)
1195+ s.Tool.TodoStatusNote = lipgloss.NewStyle().Foreground(fgSubtle)
1196+ s.Tool.TodoItem = lipgloss.NewStyle().Foreground(fgBase)
1197+ s.Tool.TodoJustStarted = lipgloss.NewStyle().Foreground(fgBase)
11841198
11851199 // MCP styles
11861200 s.Tool.MCPName = base.Foreground(blue)
@@ -1194,103 +1208,138 @@ func DefaultStyles() Styles {
11941208 s.Tool.MediaType = base
11951209 s.Tool.ResourceSize = base.Foreground(fgMuted)
11961210
1197− // Docker MCP styles
1198− s.Tool.DockerMCPActionAdd = base.Foreground(greenLight)
1199− s.Tool.DockerMCPActionDel = base.Foreground(red)
1211+ // Tool-call action verbs and result-list styling.
1212+ s.Tool.ActionCreate = lipgloss.NewStyle().Foreground(greenLight)
1213+ s.Tool.ActionDestroy = lipgloss.NewStyle().Foreground(red)
1214+ s.Tool.ResultEmpty = lipgloss.NewStyle().Foreground(fgSubtle)
1215+ s.Tool.ResultTruncation = lipgloss.NewStyle().Foreground(fgSubtle)
1216+ s.Tool.ResultItemName = lipgloss.NewStyle().Foreground(fgBase)
1217+ s.Tool.ResultItemDesc = lipgloss.NewStyle().Foreground(fgSubtle)
12001218
12011219 // Buttons
1202− s.ButtonFocus = lipgloss.NewStyle().Foreground(white).Background(secondary)
1203− s.ButtonBlur = s.Base.Background(bgSubtle)
1204−
1205− // Borders
1206− s.BorderFocus = lipgloss.NewStyle().BorderForeground(borderFocus).Border(lipgloss.RoundedBorder()).Padding(1, 2)
1220+ s.Button.Focused = lipgloss.NewStyle().Foreground(white).Background(secondary)
1221+ s.Button.Blurred = lipgloss.NewStyle().Foreground(fgBase).Background(bgSubtle)
12071222
12081223 // Editor
1209− s.EditorPromptNormalFocused = lipgloss.NewStyle().Foreground(greenDark).SetString("::: ")
1210− s.EditorPromptNormalBlurred = s.EditorPromptNormalFocused.Foreground(fgMuted)
1211− s.EditorPromptYoloIconFocused = lipgloss.NewStyle().MarginRight(1).Foreground(charmtone.Oyster).Background(charmtone.Citron).Bold(true).SetString(" ! ")
1212− s.EditorPromptYoloIconBlurred = s.EditorPromptYoloIconFocused.Foreground(charmtone.Pepper).Background(charmtone.Squid)
1213− s.EditorPromptYoloDotsFocused = lipgloss.NewStyle().MarginRight(1).Foreground(charmtone.Zest).SetString(":::")
1214− s.EditorPromptYoloDotsBlurred = s.EditorPromptYoloDotsFocused.Foreground(charmtone.Squid)
1215−
1216− s.RadioOn = s.HalfMuted.SetString(RadioOn)
1217− s.RadioOff = s.HalfMuted.SetString(RadioOff)
1218−
1219− // Logo colors
1220− s.LogoFieldColor = primary
1221− s.LogoTitleColorA = secondary
1222− s.LogoTitleColorB = primary
1223− s.LogoCharmColor = secondary
1224− s.LogoVersionColor = primary
1224+ s.Editor.PromptNormalFocused = lipgloss.NewStyle().Foreground(greenDark).SetString("::: ")
1225+ s.Editor.PromptNormalBlurred = s.Editor.PromptNormalFocused.Foreground(fgMuted)
1226+ s.Editor.PromptYoloIconFocused = lipgloss.NewStyle().MarginRight(1).Foreground(charmtone.Oyster).Background(charmtone.Citron).Bold(true).SetString(" ! ")
1227+ s.Editor.PromptYoloIconBlurred = s.Editor.PromptYoloIconFocused.Foreground(charmtone.Pepper).Background(charmtone.Squid)
1228+ s.Editor.PromptYoloDotsFocused = lipgloss.NewStyle().MarginRight(1).Foreground(charmtone.Zest).SetString(":::")
1229+ s.Editor.PromptYoloDotsBlurred = s.Editor.PromptYoloDotsFocused.Foreground(charmtone.Squid)
1230+
1231+ s.Radio.On = lipgloss.NewStyle().Foreground(fgHalfMuted).SetString(RadioOn)
1232+ s.Radio.Off = lipgloss.NewStyle().Foreground(fgHalfMuted).SetString(RadioOff)
1233+ s.Radio.Label = lipgloss.NewStyle().Foreground(fgHalfMuted)
1234+
1235+ // Logo
1236+ s.Logo.FieldColor = primary
1237+ s.Logo.TitleColorA = secondary
1238+ s.Logo.TitleColorB = primary
1239+ s.Logo.CharmColor = secondary
1240+ s.Logo.VersionColor = primary
1241+ s.Logo.SmallCharm = lipgloss.NewStyle().Foreground(secondary)
1242+ s.Logo.SmallDiagonals = lipgloss.NewStyle().Foreground(primary)
1243+ s.Logo.GradCanvas = lipgloss.NewStyle()
1244+ s.Logo.SmallGradFromColor = secondary
1245+ s.Logo.SmallGradToColor = primary
12251246
12261247 // Section
1227− s.Section.Title = s.Subtle
1228− s.Section.Line = s.Base.Foreground(charmtone.Charcoal)
1248+ s.Section.Title = subtle
1249+ s.Section.Line = base.Foreground(charmtone.Charcoal)
12291250
12301251 // Initialize
1231− s.Initialize.Header = s.Base
1232− s.Initialize.Content = s.Muted
1233− s.Initialize.Accent = s.Base.Foreground(greenDark)
1234−
1235− // LSP and MCP status.
1236− s.ResourceGroupTitle = lipgloss.NewStyle().Foreground(charmtone.Oyster)
1237− s.ResourceOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Iron).SetString("●")
1238− s.ResourceBusyIcon = s.ResourceOfflineIcon.Foreground(charmtone.Citron)
1239− s.ResourceErrorIcon = s.ResourceOfflineIcon.Foreground(charmtone.Coral)
1240− s.ResourceOnlineIcon = s.ResourceOfflineIcon.Foreground(charmtone.Guac)
1241− s.ResourceName = lipgloss.NewStyle().Foreground(charmtone.Squid)
1242− s.ResourceStatus = lipgloss.NewStyle().Foreground(charmtone.Oyster)
1243− s.ResourceAdditionalText = lipgloss.NewStyle().Foreground(charmtone.Oyster)
1252+ s.Initialize.Header = base
1253+ s.Initialize.Content = muted
1254+ s.Initialize.Accent = base.Foreground(greenDark)
1255+
1256+ // ResourceGroup (LSP/MCP/skills sidebar lists).
1257+ s.Resource.Heading = lipgloss.NewStyle().Foreground(charmtone.Oyster)
1258+ s.Resource.Name = lipgloss.NewStyle().Foreground(charmtone.Squid)
1259+ s.Resource.StatusText = lipgloss.NewStyle().Foreground(charmtone.Oyster)
1260+ s.Resource.OfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Iron).SetString("●")
1261+ s.Resource.BusyIcon = s.Resource.OfflineIcon.Foreground(charmtone.Citron)
1262+ s.Resource.ErrorIcon = s.Resource.OfflineIcon.Foreground(charmtone.Coral)
1263+ s.Resource.OnlineIcon = s.Resource.OfflineIcon.Foreground(charmtone.Guac)
1264+ s.Resource.DisabledIcon = lipgloss.NewStyle().Foreground(fgMuted).SetString("●")
1265+ s.Resource.AdditionalText = lipgloss.NewStyle().Foreground(charmtone.Oyster)
1266+ s.Resource.CapabilityCount = lipgloss.NewStyle().Foreground(fgSubtle)
1267+ s.Resource.RowTitleBase = lipgloss.NewStyle().Foreground(fgBase)
1268+ s.Resource.RowDescBase = lipgloss.NewStyle().Foreground(fgBase)
1269+ s.Resource.DefaultTitleFg = fgMuted
1270+ s.Resource.DefaultDescFg = fgSubtle
12441271
12451272 // LSP
1246− s.LSP.ErrorDiagnostic = s.Base.Foreground(redDark)
1247− s.LSP.WarningDiagnostic = s.Base.Foreground(warning)
1248− s.LSP.HintDiagnostic = s.Base.Foreground(fgHalfMuted)
1249− s.LSP.InfoDiagnostic = s.Base.Foreground(info)
1273+ s.LSP.ErrorDiagnostic = base.Foreground(redDark)
1274+ s.LSP.WarningDiagnostic = base.Foreground(warning)
1275+ s.LSP.HintDiagnostic = base.Foreground(fgHalfMuted)
1276+ s.LSP.InfoDiagnostic = base.Foreground(info)
12501277
12511278 // Files
1252− s.Files.Path = s.Muted
1253− s.Files.Additions = s.Base.Foreground(greenDark)
1254− s.Files.Deletions = s.Base.Foreground(redDark)
1279+ s.Files.Path = lipgloss.NewStyle().Foreground(fgMuted)
1280+ s.Files.Additions = lipgloss.NewStyle().Foreground(greenDark)
1281+ s.Files.Deletions = lipgloss.NewStyle().Foreground(redDark)
1282+ s.Files.SectionTitle = lipgloss.NewStyle().Foreground(fgSubtle)
1283+ s.Files.EmptyMessage = lipgloss.NewStyle().Foreground(fgSubtle)
1284+ s.Files.TruncationHint = lipgloss.NewStyle().Foreground(fgSubtle)
1285+
1286+ // Sidebar
1287+ s.Sidebar.SessionTitle = lipgloss.NewStyle().Foreground(fgMuted)
1288+ s.Sidebar.WorkingDir = lipgloss.NewStyle().Foreground(fgMuted)
1289+
1290+ // ModelInfo
1291+ s.ModelInfo.Icon = lipgloss.NewStyle().Foreground(fgSubtle)
1292+ s.ModelInfo.Name = lipgloss.NewStyle().Foreground(fgBase)
1293+ s.ModelInfo.Provider = lipgloss.NewStyle().Foreground(fgMuted)
1294+ s.ModelInfo.ProviderFallback = lipgloss.NewStyle().Foreground(fgMuted).PaddingLeft(2)
1295+ s.ModelInfo.Reasoning = lipgloss.NewStyle().Foreground(fgSubtle).PaddingLeft(2)
1296+ s.ModelInfo.TokenCount = lipgloss.NewStyle().Foreground(fgSubtle)
1297+ s.ModelInfo.TokenPercentage = lipgloss.NewStyle().Foreground(fgMuted)
1298+ s.ModelInfo.Cost = lipgloss.NewStyle().Foreground(fgMuted)
1299+
1300+ // ResourceGroup
1301+ s.Resource.DefaultTitleFg = fgMuted
1302+ s.Resource.DefaultDescFg = fgSubtle
12551303
12561304 // Chat
12571305 messageFocussedBorder := lipgloss.Border{
12581306 Left: "▌",
12591307 }
12601308
1261− s.Chat.Message.NoContent = lipgloss.NewStyle().Foreground(fgBase)
1262− s.Chat.Message.UserBlurred = s.Chat.Message.NoContent.PaddingLeft(1).BorderLeft(true).
1309+ s.Messages.NoContent = lipgloss.NewStyle().Foreground(fgBase)
1310+ s.Messages.UserBlurred = s.Messages.NoContent.PaddingLeft(1).BorderLeft(true).
12631311 BorderForeground(primary).BorderStyle(normalBorder)
1264− s.Chat.Message.UserFocused = s.Chat.Message.NoContent.PaddingLeft(1).BorderLeft(true).
1312+ s.Messages.UserFocused = s.Messages.NoContent.PaddingLeft(1).BorderLeft(true).
12651313 BorderForeground(primary).BorderStyle(messageFocussedBorder)
1266− s.Chat.Message.AssistantBlurred = s.Chat.Message.NoContent.PaddingLeft(2)
1267− s.Chat.Message.AssistantFocused = s.Chat.Message.NoContent.PaddingLeft(1).BorderLeft(true).
1314+ s.Messages.AssistantBlurred = s.Messages.NoContent.PaddingLeft(2)
1315+ s.Messages.AssistantFocused = s.Messages.NoContent.PaddingLeft(1).BorderLeft(true).
12681316 BorderForeground(greenDark).BorderStyle(messageFocussedBorder)
1269− s.Chat.Message.Thinking = lipgloss.NewStyle().MaxHeight(10)
1270− s.Chat.Message.ErrorTag = lipgloss.NewStyle().Padding(0, 1).
1317+ s.Messages.Thinking = lipgloss.NewStyle().MaxHeight(10)
1318+ s.Messages.ErrorTag = lipgloss.NewStyle().Padding(0, 1).
12711319 Background(red).Foreground(white)
1272− s.Chat.Message.ErrorTitle = lipgloss.NewStyle().Foreground(fgHalfMuted)
1273− s.Chat.Message.ErrorDetails = lipgloss.NewStyle().Foreground(fgSubtle)
1320+ s.Messages.ErrorTitle = lipgloss.NewStyle().Foreground(fgHalfMuted)
1321+ s.Messages.ErrorDetails = lipgloss.NewStyle().Foreground(fgSubtle)
12741322
12751323 // Message item styles
1276− s.Chat.Message.ToolCallFocused = s.Muted.PaddingLeft(1).
1324+ s.Messages.ToolCallFocused = muted.PaddingLeft(1).
12771325 BorderStyle(messageFocussedBorder).
12781326 BorderLeft(true).
12791327 BorderForeground(greenDark)
1280− s.Chat.Message.ToolCallBlurred = s.Muted.PaddingLeft(2)
1328+ s.Messages.ToolCallBlurred = muted.PaddingLeft(2)
12811329 // No padding or border for compact tool calls within messages
1282− s.Chat.Message.ToolCallCompact = s.Muted
1283− s.Chat.Message.SectionHeader = s.Base.PaddingLeft(2)
1284− s.Chat.Message.AssistantInfoIcon = s.Subtle
1285− s.Chat.Message.AssistantInfoModel = s.Muted
1286− s.Chat.Message.AssistantInfoProvider = s.Subtle
1287− s.Chat.Message.AssistantInfoDuration = s.Subtle
1330+ s.Messages.ToolCallCompact = muted
1331+ s.Messages.SectionHeader = base.PaddingLeft(2)
1332+ s.Messages.AssistantInfoIcon = subtle
1333+ s.Messages.AssistantInfoModel = muted
1334+ s.Messages.AssistantInfoProvider = subtle
1335+ s.Messages.AssistantInfoDuration = subtle
1336+ s.Messages.AssistantCanceled = lipgloss.NewStyle().Foreground(fgBase).Italic(true)
12881337
12891338 // Thinking section styles
1290− s.Chat.Message.ThinkingBox = s.Subtle.Background(bgBaseLighter)
1291− s.Chat.Message.ThinkingTruncationHint = s.Muted
1292− s.Chat.Message.ThinkingFooterTitle = s.Muted
1293− s.Chat.Message.ThinkingFooterDuration = s.Subtle
1339+ s.Messages.ThinkingBox = subtle.Background(bgBaseLighter)
1340+ s.Messages.ThinkingTruncationHint = muted
1341+ s.Messages.ThinkingFooterTitle = muted
1342+ s.Messages.ThinkingFooterDuration = subtle
12941343
12951344 // Text selection.
12961345 s.TextSelection = lipgloss.NewStyle().Foreground(charmtone.Salt).Background(charmtone.Charple)
@@ -1300,6 +1349,25 @@ func DefaultStyles() Styles {
13001349 s.Dialog.TitleText = base.Foreground(primary)
13011350 s.Dialog.TitleError = base.Foreground(red)
13021351 s.Dialog.TitleAccent = base.Foreground(green).Bold(true)
1352+ s.Dialog.TitleLineBase = lipgloss.NewStyle()
1353+ s.Dialog.TitleGradFromColor = primary
1354+ s.Dialog.TitleGradToColor = secondary
1355+
1356+ // Dialog.ListItem (commands, reasoning, models)
1357+ s.Dialog.ListItem.InfoBlurred = lipgloss.NewStyle().Foreground(fgBase)
1358+ s.Dialog.ListItem.InfoFocused = lipgloss.NewStyle().Foreground(fgBase)
1359+
1360+ // Dialog.Models
1361+ s.Dialog.Models.ConfiguredText = lipgloss.NewStyle().Foreground(fgSubtle)
1362+
1363+ // Dialog.Permissions
1364+ s.Dialog.Permissions.KeyText = lipgloss.NewStyle().Foreground(fgMuted)
1365+ s.Dialog.Permissions.ValueText = lipgloss.NewStyle().Foreground(fgBase)
1366+ s.Dialog.Permissions.ParamsBg = bgSubtle
1367+
1368+ // Dialog.Quit
1369+ s.Dialog.Quit.Content = lipgloss.NewStyle().Foreground(fgBase)
1370+ s.Dialog.Quit.Frame = lipgloss.NewStyle().BorderForeground(borderFocus).Border(lipgloss.RoundedBorder()).Padding(1, 2)
13031371 s.Dialog.View = base.Border(lipgloss.RoundedBorder()).BorderForeground(borderFocus)
13041372 s.Dialog.PrimaryText = base.Padding(0, 1).Foreground(primary)
13051373 s.Dialog.SecondaryText = base.Padding(0, 1).Foreground(fgSubtle)
@@ -1323,6 +1391,20 @@ func DefaultStyles() Styles {
13231391
13241392 s.Dialog.ImagePreview = lipgloss.NewStyle().Padding(0, 1).Foreground(fgSubtle)
13251393
1394+ // API key input dialog
1395+ s.Dialog.APIKey.Spinner = base.Foreground(green)
1396+
1397+ // OAuth dialog
1398+ s.Dialog.OAuth.Spinner = base.Foreground(greenLight)
1399+ s.Dialog.OAuth.Instructions = lipgloss.NewStyle().Foreground(white)
1400+ s.Dialog.OAuth.UserCode = lipgloss.NewStyle().Bold(true).Foreground(white)
1401+ s.Dialog.OAuth.Success = lipgloss.NewStyle().Foreground(greenLight)
1402+ s.Dialog.OAuth.Link = lipgloss.NewStyle().Foreground(greenDark).Underline(true)
1403+ s.Dialog.OAuth.Enter = lipgloss.NewStyle().Foreground(primary)
1404+ s.Dialog.OAuth.ErrorText = lipgloss.NewStyle().Foreground(error)
1405+ s.Dialog.OAuth.StatusText = lipgloss.NewStyle().Foreground(fgMuted)
1406+ s.Dialog.OAuth.UserCodeBg = bgBaseLighter
1407+
13261408 s.Dialog.Arguments.Content = base.Padding(1)
13271409 s.Dialog.Arguments.Description = base.MarginBottom(1).MaxHeight(3)
13281410 s.Dialog.Arguments.InputLabelBlurred = base.Foreground(fgMuted)
@@ -1332,20 +1414,22 @@ func DefaultStyles() Styles {
13321414
13331415 s.Dialog.Sessions.DeletingTitle = s.Dialog.Title.Foreground(red)
13341416 s.Dialog.Sessions.DeletingView = s.Dialog.View.BorderForeground(red)
1335− s.Dialog.Sessions.DeletingMessage = s.Base.Padding(1)
1417+ s.Dialog.Sessions.DeletingMessage = base.Padding(1)
13361418 s.Dialog.Sessions.DeletingTitleGradientFromColor = red
1337− s.Dialog.Sessions.DeletingTitleGradientToColor = s.Primary
1419+ s.Dialog.Sessions.DeletingTitleGradientToColor = primary
13381420 s.Dialog.Sessions.DeletingItemBlurred = s.Dialog.NormalItem.Foreground(fgSubtle)
13391421 s.Dialog.Sessions.DeletingItemFocused = s.Dialog.SelectedItem.Background(red).Foreground(charmtone.Butter)
13401422
13411423 s.Dialog.Sessions.RenamingingTitle = s.Dialog.Title.Foreground(charmtone.Zest)
13421424 s.Dialog.Sessions.RenamingView = s.Dialog.View.BorderForeground(charmtone.Zest)
1343− s.Dialog.Sessions.RenamingingMessage = s.Base.Padding(1)
1425+ s.Dialog.Sessions.RenamingingMessage = base.Padding(1)
13441426 s.Dialog.Sessions.RenamingTitleGradientFromColor = charmtone.Zest
13451427 s.Dialog.Sessions.RenamingTitleGradientToColor = charmtone.Bok
13461428 s.Dialog.Sessions.RenamingItemBlurred = s.Dialog.NormalItem.Foreground(fgSubtle)
13471429 s.Dialog.Sessions.RenamingingItemFocused = s.Dialog.SelectedItem.UnsetBackground().UnsetForeground()
13481430 s.Dialog.Sessions.RenamingPlaceholder = base.Foreground(charmtone.Squid)
1431+ s.Dialog.Sessions.InfoBlurred = lipgloss.NewStyle().Foreground(fgSubtle)
1432+ s.Dialog.Sessions.InfoFocused = lipgloss.NewStyle().Foreground(fgBase)
13491433
13501434 s.Status.Help = lipgloss.NewStyle().Padding(0, 1)
13511435 s.Status.SuccessIndicator = base.Foreground(bgSubtle).Background(green).Padding(0, 1).Bold(true).SetString("OKAY!")
@@ -1375,11 +1459,19 @@ func DefaultStyles() Styles {
13751459 s.Pills.Base = base.Padding(0, 1)
13761460 s.Pills.Focused = base.Padding(0, 1).BorderStyle(lipgloss.RoundedBorder()).BorderForeground(bgOverlay)
13771461 s.Pills.Blurred = base.Padding(0, 1).BorderStyle(lipgloss.HiddenBorder())
1378− s.Pills.QueueItemPrefix = s.Muted.SetString(" •")
1379− s.Pills.HelpKey = s.Muted
1380− s.Pills.HelpText = s.Subtle
1462+ s.Pills.QueueItemPrefix = lipgloss.NewStyle().Foreground(fgMuted).SetString(" •")
1463+ s.Pills.QueueItemText = lipgloss.NewStyle().Foreground(fgMuted)
1464+ s.Pills.QueueLabel = lipgloss.NewStyle().Foreground(fgBase)
1465+ s.Pills.QueueIconBase = lipgloss.NewStyle().Foreground(fgBase)
1466+ s.Pills.QueueGradFromColor = redDark
1467+ s.Pills.QueueGradToColor = secondary
1468+ s.Pills.TodoLabel = lipgloss.NewStyle().Foreground(fgBase)
1469+ s.Pills.TodoProgress = lipgloss.NewStyle().Foreground(fgMuted)
1470+ s.Pills.TodoCurrentTask = lipgloss.NewStyle().Foreground(fgSubtle)
1471+ s.Pills.TodoSpinner = lipgloss.NewStyle().Foreground(greenDark)
1472+ s.Pills.HelpKey = lipgloss.NewStyle().Foreground(fgMuted)
1473+ s.Pills.HelpText = lipgloss.NewStyle().Foreground(fgSubtle)
13811474 s.Pills.Area = base
1382− s.Pills.TodoSpinner = base.Foreground(greenDark)
13831475
13841476 return s
13851477}