on main help sign in
refactor: compact mode (#1850)
commit19c67f7ffc90d1470a4de5644ac78733f4020a42 authorKujtim Hoxha <kujtimii.h@gmail.com> dateTue 13 Jan 2026, 16:58 (9 months ago) parent7a2769c
8 files changed+366−78
internal/ui/dialog/commands.go+1−1 internal/ui/model/header.go+112 internal/ui/model/lsp.go+3 internal/ui/model/mcp.go+3 internal/ui/model/session.go+9−2 internal/ui/model/sidebar.go+1−1 internal/ui/model/ui.go+209−68 internal/ui/styles/styles.go+28−6
internal/ui/dialog/commands.go+1−1
@@ -227,11 +227,11 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
227227 width := max(0, min(defaultDialogMaxWidth, area.Dx()))
228228 height := max(0, min(defaultDialogHeight, area.Dy()))
229229 if area.Dx() != c.windowWidth && c.selected == SystemCommands {
230+ c.windowWidth = area.Dx()
230231 // since some items in the list depend on width (e.g. toggle sidebar command),
231232 // we need to reset the command items when width changes
232233 c.setCommandItems(c.selected)
233234 }
234− c.windowWidth = area.Dx()
235235 innerWidth := width - c.com.Styles.Dialog.View.GetHorizontalFrameSize()
236236 heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight +
237237 t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight +
internal/ui/model/header.gonew file+112
@@ -0,0 +1,112 @@
1+package model
2+
3+import (
4+ "fmt"
5+ "strings"
6+
7+ "charm.land/lipgloss/v2"
8+ "github.com/charmbracelet/crush/internal/config"
9+ "github.com/charmbracelet/crush/internal/csync"
10+ "github.com/charmbracelet/crush/internal/fsext"
11+ "github.com/charmbracelet/crush/internal/lsp"
12+ "github.com/charmbracelet/crush/internal/session"
13+ "github.com/charmbracelet/crush/internal/ui/common"
14+ "github.com/charmbracelet/crush/internal/ui/styles"
15+ "github.com/charmbracelet/x/ansi"
16+)
17+
18+const (
19+ headerDiag = "╱"
20+ minHeaderDiags = 3
21+ leftPadding = 1
22+ rightPadding = 1
23+)
24+
25+// renderCompactHeader renders the compact header for the given session.
26+func renderCompactHeader(
27+ com *common.Common,
28+ session *session.Session,
29+ lspClients *csync.Map[string, *lsp.Client],
30+ detailsOpen bool,
31+ width int,
32+) string {
33+ if session == nil || session.ID == "" {
34+ return ""
35+ }
36+
37+ t := com.Styles
38+
39+ var b strings.Builder
40+
41+ b.WriteString(t.Header.Charm.Render("Charm™"))
42+ b.WriteString(" ")
43+ b.WriteString(styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary))
44+ b.WriteString(" ")
45+
46+ availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags
47+ details := renderHeaderDetails(com, session, lspClients, detailsOpen, availDetailWidth)
48+
49+ remainingWidth := width -
50+ lipgloss.Width(b.String()) -
51+ lipgloss.Width(details) -
52+ leftPadding -
53+ rightPadding
54+
55+ if remainingWidth > 0 {
56+ b.WriteString(t.Header.Diagonals.Render(
57+ strings.Repeat(headerDiag, max(minHeaderDiags, remainingWidth)),
58+ ))
59+ b.WriteString(" ")
60+ }
61+
62+ b.WriteString(details)
63+
64+ return t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String())
65+}
66+
67+// renderHeaderDetails renders the details section of the header.
68+func renderHeaderDetails(
69+ com *common.Common,
70+ session *session.Session,
71+ lspClients *csync.Map[string, *lsp.Client],
72+ detailsOpen bool,
73+ availWidth int,
74+) string {
75+ t := com.Styles
76+
77+ var parts []string
78+
79+ errorCount := 0
80+ for l := range lspClients.Seq() {
81+ errorCount += l.GetDiagnosticCounts().Error
82+ }
83+
84+ if errorCount > 0 {
85+ parts = append(parts, t.LSP.ErrorDiagnostic.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount)))
86+ }
87+
88+ agentCfg := config.Get().Agents[config.AgentCoder]
89+ model := config.Get().GetModelByType(agentCfg.Model)
90+ percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
91+ formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage)))
92+ parts = append(parts, formattedPercentage)
93+
94+ const keystroke = "ctrl+d"
95+ if detailsOpen {
96+ parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" close"))
97+ } else {
98+ parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" open "))
99+ }
100+
101+ dot := t.Header.Separator.Render(" • ")
102+ metadata := strings.Join(parts, dot)
103+ metadata = dot + metadata
104+
105+ const dirTrimLimit = 4
106+ cfg := com.Config()
107+ cwd := fsext.DirTrim(fsext.PrettyPath(cfg.WorkingDir()), dirTrimLimit)
108+ cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "…")
109+ cwd = t.Header.WorkingDir.Render(cwd)
110+
111+ return cwd + metadata
112+}
internal/ui/model/lsp.go+3
@@ -72,6 +72,9 @@ func lspDiagnostics(t *styles.Styles, diagnostics map[protocol.DiagnosticSeverit
7272// lspList renders a list of LSP clients with their status and diagnostics,
7373// truncating to maxItems if needed.
7474func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string {
75+ if maxItems <= 0 {
76+ return ""
77+ }
7578 var renderedLsps []string
7679 for _, l := range lsps {
7780 var icon string
internal/ui/model/mcp.go+3
@@ -49,6 +49,9 @@ func mcpCounts(t *styles.Styles, counts mcp.Counts) string {
4949// mcpList renders a list of MCP clients with their status and counts,
5050// truncating to maxItems if needed.
5151func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) string {
52+ if maxItems <= 0 {
53+ return ""
54+ }
5255 var renderedMcps []string
5356
5457 for _, m := range mcps {
internal/ui/model/session.go+9−2
@@ -169,9 +169,13 @@ func (m *UI) handleFileEvent(file history.File) tea.Cmd {
169169
170170// filesInfo renders the modified files section for the sidebar, showing files
171171// with their addition/deletion counts.
172−func (m *UI) filesInfo(cwd string, width, maxItems int) string {
172+func (m *UI) filesInfo(cwd string, width, maxItems int, isSection bool) string {
173173 t := m.com.Styles
174− title := common.Section(t, "Modified Files", width)
174+
175+ title := t.Subtle.Render("Modified Files")
176+ if isSection {
177+ title = common.Section(t, "Modified Files", width)
178+ }
175179 list := t.Subtle.Render("None")
176180
177181 if len(m.sessionFiles) > 0 {
@@ -184,6 +188,9 @@ func (m *UI) filesInfo(cwd string, width, maxItems int) string {
184188// fileList renders a list of files with their diff statistics, truncating to
185189// maxItems and showing a "...and N more" message if needed.
186190func fileList(t *styles.Styles, cwd string, files []SessionFile, width, maxItems int) string {
191+ if maxItems <= 0 {
192+ return ""
193+ }
187194 var renderedFiles []string
188195 filesShown := 0
189196
internal/ui/model/sidebar.go+1−1
@@ -133,7 +133,7 @@ func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) {
133133
134134 lspSection := m.lspInfo(width, maxLSPs, true)
135135 mcpSection := m.mcpInfo(width, maxMCPs, true)
136− filesSection := m.filesInfo(m.com.Config().WorkingDir(), width, maxFiles)
136+ filesSection := m.filesInfo(m.com.Config().WorkingDir(), width, maxFiles, true)
137137
138138 uv.NewStyledString(
139139 lipgloss.NewStyle().
internal/ui/model/ui.go+209−68
@@ -53,6 +53,15 @@ const maxAttachmentSize = int64(5 * 1024 * 1024)
5353// Allowed image formats.
5454var allowedImageTypes = []string{".jpg", ".jpeg", ".png"}
5555
56+// Compact mode breakpoints.
57+const (
58+ compactModeWidthBreakpoint = 120
59+ compactModeHeightBreakpoint = 30
60+)
61+
62+// Session details panel max height.
63+const sessionDetailsMaxHeight = 20
64+
5665// uiFocusState represents the current focus state of the UI.
5766type uiFocusState uint8
5867
@@ -71,7 +80,6 @@ const (
7180 uiInitialize
7281 uiLanding
7382 uiChat
74− uiChatCompact
7583)
7684
7785type openEditorMsg struct {
@@ -161,6 +169,16 @@ type UI struct {
161169 // custom commands & mcp commands
162170 customCommands []commands.CustomCommand
163171 mcpCustomCommands []commands.MCPCustomCommand
172+
173+ // forceCompactMode tracks whether compact mode is forced by user toggle
174+ forceCompactMode bool
175+
176+ // isCompact tracks whether we're currently in compact layout mode (either
177+ // by user toggle or auto-switch based on window size)
178+ isCompact bool
179+
180+ // detailsOpen tracks whether the details panel is open (in compact mode)
181+ detailsOpen bool
164182}
165183
166184// New creates a new instance of the [UI] model.
@@ -233,6 +251,9 @@ func New(com *common.Common) *UI {
233251 ui.textarea.Placeholder = ui.readyPlaceholder
234252 ui.status = status
235253
254+ // Initialize compact mode from config
255+ ui.forceCompactMode = com.Config().Options.TUI.CompactMode
256+
236257 return ui
237258}
238259
@@ -284,6 +305,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
284305 }
285306 case loadSessionMsg:
286307 m.state = uiChat
308+ if m.forceCompactMode {
309+ m.isCompact = true
310+ }
287311 m.session = msg.session
288312 m.sessionFiles = msg.files
289313 msgs, err := m.com.App.Messages.List(context.Background(), m.session.ID)
@@ -370,6 +394,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
370394 return m, nil
371395 case tea.WindowSizeMsg:
372396 m.width, m.height = msg.Width, msg.Height
397+ m.handleCompactMode(m.width, m.height)
373398 m.updateLayoutAndSize()
374399 case tea.KeyboardEnhancementsMsg:
375400 m.keyenh = msg
@@ -840,6 +865,9 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
840865 case dialog.ActionToggleHelp:
841866 m.status.ToggleHelp()
842867 m.dialog.CloseDialog(dialog.CommandsID)
868+ case dialog.ActionToggleCompactMode:
869+ cmds = append(cmds, m.toggleCompactMode())
870+ m.dialog.CloseDialog(dialog.CommandsID)
843871 case dialog.ActionQuit:
844872 cmds = append(cmds, tea.Quit)
845873 case dialog.ActionInitializeProject:
@@ -938,6 +966,10 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
938966 cmds = append(cmds, cmd)
939967 }
940968 return true
969+ case key.Matches(msg, m.keyMap.Chat.Details) && m.isCompact:
970+ m.detailsOpen = !m.detailsOpen
971+ m.updateLayoutAndSize()
972+ return true
941973 }
942974 return false
943975 }
@@ -972,7 +1004,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
9721004 case uiInitialize:
9731005 cmds = append(cmds, m.updateInitializeView(msg)...)
9741006 return tea.Batch(cmds...)
975− case uiChat, uiLanding, uiChatCompact:
1007+ case uiChat, uiLanding:
9761008 switch m.focus {
9771009 case uiFocusEditor:
9781010 // Handle completions if open.
@@ -1070,6 +1102,12 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
10701102 }
10711103 }
10721104
1105+ // remove the details if they are open when user starts typing
1106+ if m.detailsOpen {
1107+ m.detailsOpen = false
1108+ m.updateLayoutAndSize()
1109+ }
1110+
10731111 ta, cmd := m.textarea.Update(msg)
10741112 m.textarea = ta
10751113 cmds = append(cmds, cmd)
@@ -1220,28 +1258,26 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
12201258 editor.Draw(scr, layout.editor)
12211259
12221260 case uiChat:
1223− m.chat.Draw(scr, layout.main)
1261+ if m.isCompact {
1262+ header := uv.NewStyledString(m.header)
1263+ header.Draw(scr, layout.header)
1264+ } else {
1265+ m.drawSidebar(scr, layout.sidebar)
1266+ }
12241267
1225− header := uv.NewStyledString(m.header)
1226− header.Draw(scr, layout.header)
1227− m.drawSidebar(scr, layout.sidebar)
1268+ m.chat.Draw(scr, layout.main)
12281269
1229− editor := uv.NewStyledString(m.renderEditorView(scr.Bounds().Dx() - layout.sidebar.Dx()))
1270+ editorWidth := scr.Bounds().Dx()
1271+ if !m.isCompact {
1272+ editorWidth -= layout.sidebar.Dx()
1273+ }
1274+ editor := uv.NewStyledString(m.renderEditorView(editorWidth))
12301275 editor.Draw(scr, layout.editor)
12311276
1232− case uiChatCompact:
1233− header := uv.NewStyledString(m.header)
1234− header.Draw(scr, layout.header)
1235−
1236− mainView := lipgloss.NewStyle().Width(layout.main.Dx()).
1237− Height(layout.main.Dy()).
1238− Background(lipgloss.ANSIColor(rand.Intn(256))).
1239− Render(" Compact Chat Messages ")
1240− main := uv.NewStyledString(mainView)
1241− main.Draw(scr, layout.main)
1242−
1243− editor := uv.NewStyledString(m.renderEditorView(scr.Bounds().Dx()))
1244− editor.Draw(scr, layout.editor)
1277+ // Draw details overlay in compact mode when open
1278+ if m.isCompact && m.detailsOpen {
1279+ m.drawSessionDetails(scr, layout.sessionDetails)
1280+ }
12451281 }
12461282
12471283 // Add status and help layer
@@ -1290,6 +1326,10 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
12901326 // Don't show cursor if editor is not visible
12911327 return nil
12921328 }
1329+ if m.detailsOpen && m.isCompact {
1330+ // Don't show cursor if details overlay is open
1331+ return nil
1332+ }
12931333
12941334 if m.textarea.Focused() {
12951335 cur := m.textarea.Cursor()
@@ -1537,6 +1577,36 @@ func (m *UI) FullHelp() [][]key.Binding {
15371577 return binds
15381578}
15391579
1580+// toggleCompactMode toggles compact mode between uiChat and uiChatCompact states.
1581+func (m *UI) toggleCompactMode() tea.Cmd {
1582+ m.forceCompactMode = !m.forceCompactMode
1583+
1584+ err := m.com.Config().SetCompactMode(m.forceCompactMode)
1585+ if err != nil {
1586+ return uiutil.ReportError(err)
1587+ }
1588+
1589+ m.handleCompactMode(m.width, m.height)
1590+ m.updateLayoutAndSize()
1591+
1592+ return nil
1593+}
1594+
1595+// handleCompactMode updates the UI state based on window size and compact mode setting.
1596+func (m *UI) handleCompactMode(newWidth, newHeight int) {
1597+ if m.state == uiChat {
1598+ if m.forceCompactMode {
1599+ m.isCompact = true
1600+ return
1601+ }
1602+ if newWidth < compactModeWidthBreakpoint || newHeight < compactModeHeightBreakpoint {
1603+ m.isCompact = true
1604+ } else {
1605+ m.isCompact = false
1606+ }
1607+ }
1608+}
1609+
15401610// updateLayoutAndSize updates the layout and sizes of UI components.
15411611func (m *UI) updateLayoutAndSize() {
15421612 m.layout = m.generateLayout(m.width, m.height)
@@ -1558,11 +1628,11 @@ func (m *UI) updateSize() {
15581628 m.renderHeader(false, m.layout.header.Dx())
15591629
15601630 case uiChat:
1561− m.renderSidebarLogo(m.layout.sidebar.Dx())
1562−
1563− case uiChatCompact:
1564− // TODO: set the width and heigh of the chat component
1565− m.renderHeader(true, m.layout.header.Dx())
1631+ if m.isCompact {
1632+ m.renderHeader(true, m.layout.header.Dx())
1633+ } else {
1634+ m.renderSidebarLogo(m.layout.sidebar.Dx())
1635+ }
15661636 }
15671637}
15681638
@@ -1579,8 +1649,7 @@ func (m *UI) generateLayout(w, h int) layout {
15791649 // The sidebar width
15801650 sidebarWidth := 30
15811651 // The header height
1582− // TODO: handle compact
1583− headerHeight := 4
1652+ const landingHeaderHeight = 4
15841653
15851654 var helpKeyMap help.KeyMap = m
15861655 if m.status.ShowingAll() {
@@ -1619,7 +1688,7 @@ func (m *UI) generateLayout(w, h int) layout {
16191688 // ------
16201689 // help
16211690
1622− headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(headerHeight))
1691+ headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(landingHeaderHeight))
16231692 layout.header = headerRect
16241693 layout.main = mainRect
16251694
@@ -1633,7 +1702,7 @@ func (m *UI) generateLayout(w, h int) layout {
16331702 // editor
16341703 // ------
16351704 // help
1636− headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(headerHeight))
1705+ headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(landingHeaderHeight))
16371706 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
16381707 // Remove extra padding from editor (but keep it for header and main)
16391708 editorRect.Min.X -= 1
@@ -1643,41 +1712,52 @@ func (m *UI) generateLayout(w, h int) layout {
16431712 layout.editor = editorRect
16441713
16451714 case uiChat:
1646− // Layout
1647− //
1648− // ------|---
1649− // main |
1650− // ------| side
1651− // editor|
1652− // ----------
1653− // help
1654−
1655− mainRect, sideRect := uv.SplitHorizontal(appRect, uv.Fixed(appRect.Dx()-sidebarWidth))
1656− // Add padding left
1657− sideRect.Min.X += 1
1658− mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
1659− mainRect.Max.X -= 1 // Add padding right
1660− // Add bottom margin to main
1661− mainRect.Max.Y -= 1
1662− layout.sidebar = sideRect
1663− layout.main = mainRect
1664− layout.editor = editorRect
1665−
1666− case uiChatCompact:
1667− // Layout
1668− //
1669− // compact-header
1670− // ------
1671− // main
1672− // ------
1673− // editor
1674− // ------
1675− // help
1676− headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(appRect.Dy()-headerHeight))
1677− mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
1678− layout.header = headerRect
1679− layout.main = mainRect
1680− layout.editor = editorRect
1715+ if m.isCompact {
1716+ // Layout
1717+ //
1718+ // compact-header
1719+ // ------
1720+ // main
1721+ // ------
1722+ // editor
1723+ // ------
1724+ // help
1725+ const compactHeaderHeight = 1
1726+ headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(compactHeaderHeight))
1727+ detailsHeight := min(sessionDetailsMaxHeight, area.Dy()-1) // One row for the header
1728+ sessionDetailsArea, _ := uv.SplitVertical(appRect, uv.Fixed(detailsHeight))
1729+ layout.sessionDetails = sessionDetailsArea
1730+ layout.sessionDetails.Min.Y += compactHeaderHeight // adjust for header
1731+ // Add one line gap between header and main content
1732+ mainRect.Min.Y += 1
1733+ mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
1734+ mainRect.Max.X -= 1 // Add padding right
1735+ // Add bottom margin to main
1736+ mainRect.Max.Y -= 1
1737+ layout.header = headerRect
1738+ layout.main = mainRect
1739+ layout.editor = editorRect
1740+ } else {
1741+ // Layout
1742+ //
1743+ // ------|---
1744+ // main |
1745+ // ------| side
1746+ // editor|
1747+ // ----------
1748+ // help
1749+
1750+ mainRect, sideRect := uv.SplitHorizontal(appRect, uv.Fixed(appRect.Dx()-sidebarWidth))
1751+ // Add padding left
1752+ sideRect.Min.X += 1
1753+ mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
1754+ mainRect.Max.X -= 1 // Add padding right
1755+ // Add bottom margin to main
1756+ mainRect.Max.Y -= 1
1757+ layout.sidebar = sideRect
1758+ layout.main = mainRect
1759+ layout.editor = editorRect
1760+ }
16811761 }
16821762
16831763 if !layout.editor.Empty() {
@@ -1711,6 +1791,9 @@ type layout struct {
17111791
17121792 // status is the area for the status view.
17131793 status uv.Rectangle
1794+
1795+ // session details is the area for the session details overlay in compact mode.
1796+ sessionDetails uv.Rectangle
17141797}
17151798
17161799func (m *UI) openEditor(value string) tea.Cmd {
@@ -1916,8 +1999,11 @@ func (m *UI) renderEditorView(width int) string {
19161999
19172000// renderHeader renders and caches the header logo at the specified width.
19182001func (m *UI) renderHeader(compact bool, width int) {
1919− // TODO: handle the compact case differently
1920− m.header = renderLogo(m.com.Styles, compact, width)
2002+ if compact && m.session != nil && m.com.App != nil {
2003+ m.header = renderCompactHeader(m.com, m.session, m.com.App.LSPClients, m.detailsOpen, width)
2004+ } else {
2005+ m.header = renderLogo(m.com.Styles, compact, width)
2006+ }
19212007}
19222008
19232009// renderSidebarLogo renders and caches the sidebar logo at the specified
@@ -1939,8 +2025,13 @@ func (m *UI) sendMessage(content string, attachments []message.Attachment) tea.C
19392025 return uiutil.ReportError(err)
19402026 }
19412027 m.state = uiChat
1942− m.session = &newSession
1943− cmds = append(cmds, m.loadSession(newSession.ID))
2028+ if m.forceCompactMode {
2029+ m.isCompact = true
2030+ }
2031+ if newSession.ID != "" {
2032+ m.session = &newSession
2033+ cmds = append(cmds, m.loadSession(newSession.ID))
2034+ }
19442035 }
19452036
19462037 // Capture session ID to avoid race with main goroutine updating m.session.
@@ -2252,6 +2343,56 @@ func (m *UI) pasteIdx() int {
22522343 return result + 1
22532344}
22542345
2346+// drawSessionDetails draws the session details in compact mode.
2347+func (m *UI) drawSessionDetails(scr uv.Screen, area uv.Rectangle) {
2348+ if m.session == nil {
2349+ return
2350+ }
2351+
2352+ s := m.com.Styles
2353+
2354+ width := area.Dx() - s.CompactDetails.View.GetHorizontalFrameSize()
2355+ height := area.Dy() - s.CompactDetails.View.GetVerticalFrameSize()
2356+
2357+ title := s.CompactDetails.Title.Width(width).MaxHeight(2).Render(m.session.Title)
2358+ blocks := []string{
2359+ title,
2360+ "",
2361+ m.modelInfo(width),
2362+ "",
2363+ }
2364+
2365+ detailsHeader := lipgloss.JoinVertical(
2366+ lipgloss.Left,
2367+ blocks...,
2368+ )
2369+
2370+ version := s.CompactDetails.Version.Foreground(s.Border).Width(width).AlignHorizontal(lipgloss.Right).Render(version.Version)
2371+
2372+ remainingHeight := height - lipgloss.Height(detailsHeader) - lipgloss.Height(version)
2373+
2374+ const maxSectionWidth = 50
2375+ sectionWidth := min(maxSectionWidth, width/3-2) // account for 2 spaces
2376+ maxItemsPerSection := remainingHeight - 3 // Account for section title and spacing
2377+
2378+ lspSection := m.lspInfo(sectionWidth, maxItemsPerSection, false)
2379+ mcpSection := m.mcpInfo(sectionWidth, maxItemsPerSection, false)
2380+ filesSection := m.filesInfo(m.com.Config().WorkingDir(), sectionWidth, maxItemsPerSection, false)
2381+ sections := lipgloss.JoinHorizontal(lipgloss.Top, filesSection, " ", lspSection, " ", mcpSection)
2382+ uv.NewStyledString(
2383+ s.CompactDetails.View.
2384+ Width(area.Dx()).
2385+ Render(
2386+ lipgloss.JoinVertical(
2387+ lipgloss.Left,
2388+ detailsHeader,
2389+ sections,
2390+ version,
2391+ ),
2392+ ),
2393+ ).Draw(scr, area)
2394+}
2395+
22552396// renderLogo renders the Crush logo with the given styles and dimensions.
22562397func renderLogo(t *styles.Styles, compact bool, width int) string {
22572398 return logo.Render(version.Version, compact, logo.Opts{
internal/ui/styles/styles.go+28−6
@@ -69,9 +69,22 @@ type Styles struct {
6969 TagError lipgloss.Style
7070 TagInfo lipgloss.Style
7171
72− // Headers
73− HeaderTool lipgloss.Style
74− HeaderToolNested lipgloss.Style
72+ // Header
73+ Header struct {
74+ Charm lipgloss.Style // Style for "Charm™" label
75+ Diagonals lipgloss.Style // Style for diagonal separators (╱)
76+ Percentage lipgloss.Style // Style for context percentage
77+ Keystroke lipgloss.Style // Style for keystroke hints (e.g., "ctrl+d")
78+ KeystrokeTip lipgloss.Style // Style for keystroke action text (e.g., "open", "close")
79+ WorkingDir lipgloss.Style // Style for current working directory
80+ Separator lipgloss.Style // Style for separator dots (•)
81+ }
82+
83+ CompactDetails struct {
84+ View lipgloss.Style
85+ Version lipgloss.Style
86+ Title lipgloss.Style
87+ }
7588
7689 // Panels
7790 PanelMuted lipgloss.Style
@@ -995,9 +1008,18 @@ func DefaultStyles() Styles {
9951008 s.TagError = s.TagBase.Background(redDark)
9961009 s.TagInfo = s.TagBase.Background(blueLight)
9971010
998− // headers
999− s.HeaderTool = lipgloss.NewStyle().Foreground(blue)
1000− s.HeaderToolNested = lipgloss.NewStyle().Foreground(fgHalfMuted)
1011+ // Compact header styles
1012+ s.Header.Charm = base.Foreground(secondary)
1013+ s.Header.Diagonals = base.Foreground(primary)
1014+ s.Header.Percentage = s.Muted
1015+ s.Header.Keystroke = s.Muted
1016+ s.Header.KeystrokeTip = s.Subtle
1017+ s.Header.WorkingDir = s.Muted
1018+ s.Header.Separator = s.Subtle
1019+
1020+ s.CompactDetails.Title = s.Base
1021+ s.CompactDetails.View = s.Base.Padding(0, 1, 1, 1).Border(lipgloss.RoundedBorder()).BorderForeground(borderFocus)
1022+ s.CompactDetails.Version = s.Muted
10011023
10021024 // panels
10031025 s.PanelMuted = s.Muted.Background(bgBaseLighter)