on main help sign in
fix(ui): context percentage updates (#2115)

* fix(ui): context percentage updates When the agent is performing tasks, the context percentage in the header was not updating correctly. This commit fixes the issue by ensuring that the header always draws the context details. * fix(ui): always turn off compact mode when going to landing state

commitf53402d57eac0590d009611cf892c8957eab15e7 authorAyman Bagabas <ayman.bagabas@gmail.com> dateWed 4 Feb 2026, 12:58 (8 months ago) parent112fea8
2 files changed+75−44
internal/ui/model/header.go+47−15 internal/ui/model/ui.go+28−29
internal/ui/model/header.go+47−15
@@ -12,6 +12,7 @@ import (
1212 "github.com/charmbracelet/crush/internal/session"
1313 "github.com/charmbracelet/crush/internal/ui/common"
1414 "github.com/charmbracelet/crush/internal/ui/styles"
15+ uv "github.com/charmbracelet/ultraviolet"
1516 "github.com/charmbracelet/x/ansi"
1617)
1718
@@ -22,29 +23,58 @@ const (
2223 rightPadding = 1
2324)
2425
25−// renderCompactHeader renders the compact header for the given session.
26−func renderCompactHeader(
27− com *common.Common,
26+type header struct {
27+ // cached logo and compact logo
28+ logo string
29+ compactLogo string
30+
31+ com *common.Common
32+ width int
33+ compact bool
34+}
35+
36+// newHeader creates a new header model.
37+func newHeader(com *common.Common) *header {
38+ h := &header{
39+ com: com,
40+ }
41+ t := com.Styles
42+ h.compactLogo = t.Header.Charm.Render("Charm™") + " " +
43+ styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary) + " "
44+ return h
45+}
46+
47+// drawHeader draws the header for the given session.
48+func (h *header) drawHeader(
49+ scr uv.Screen,
50+ area uv.Rectangle,
2851 session *session.Session,
29− lspClients *csync.Map[string, *lsp.Client],
52+ compact bool,
3053 detailsOpen bool,
3154 width int,
32−) string {
33− if session == nil || session.ID == "" {
34− return ""
55+) {
56+ t := h.com.Styles
57+ if width != h.width || compact != h.compact {
58+ h.logo = renderLogo(h.com.Styles, compact, width)
3559 }
3660
37− t := com.Styles
61+ h.width = width
62+ h.compact = compact
3863
39− var b strings.Builder
64+ if !compact || session == nil || h.com.App == nil {
65+ uv.NewStyledString(h.logo).Draw(scr, area)
66+ return
67+ }
68+
69+ if session.ID == "" {
70+ return
71+ }
4072
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(" ")
73+ var b strings.Builder
74+ b.WriteString(h.compactLogo)
4575
4676 availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags
47− details := renderHeaderDetails(com, session, lspClients, detailsOpen, availDetailWidth)
77+ details := renderHeaderDetails(h.com, session, h.com.App.LSPClients, detailsOpen, availDetailWidth)
4878
4979 remainingWidth := width -
5080 lipgloss.Width(b.String()) -
@@ -61,7 +91,9 @@ func renderCompactHeader(
6191
6292 b.WriteString(details)
6393
64− return t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String())
94+ view := uv.NewStyledString(
95+ t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String()))
96+ view.Draw(scr, area)
6597}
6698
6799// renderHeaderDetails renders the details section of the header.
internal/ui/model/ui.go+28−29
@@ -141,8 +141,7 @@ type UI struct {
141141 // isCanceling tracks whether the user has pressed escape once to cancel.
142142 isCanceling bool
143143
144− // header is the last cached header logo
145− header string
144+ header *header
146145
147146 // sendProgressBar instructs the TUI to send progress bar updates to the
148147 // terminal.
@@ -261,12 +260,15 @@ func New(com *common.Common) *UI {
261260 },
262261 )
263262
263+ header := newHeader(com)
264+
264265 ui := &UI{
265266 com: com,
266267 dialog: dialog.NewOverlay(),
267268 keyMap: keyMap,
268269 textarea: ta,
269270 chat: ch,
271+ header: header,
270272 completions: comp,
271273 attachments: attachments,
272274 todoSpinner: todoSpinner,
@@ -325,6 +327,10 @@ func (m *UI) Init() tea.Cmd {
325327
326328// setState changes the UI state and focus.
327329func (m *UI) setState(state uiState, focus uiFocusState) {
330+ if state == uiLanding {
331+ // Always turn off compact mode when going to landing
332+ m.isCompact = false
333+ }
328334 m.state = state
329335 m.focus = focus
330336 // Changing the state may change layout, so update it.
@@ -1761,6 +1767,18 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
17611767 return tea.Batch(cmds...)
17621768}
17631769
1770+// drawHeader draws the header section of the UI.
1771+func (m *UI) drawHeader(scr uv.Screen, area uv.Rectangle) {
1772+ m.header.drawHeader(
1773+ scr,
1774+ area,
1775+ m.session,
1776+ m.isCompact,
1777+ m.detailsOpen,
1778+ m.width,
1779+ )
1780+}
1781+
17641782// Draw implements [uv.Drawable] and draws the UI model.
17651783func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
17661784 layout := m.generateLayout(area.Dx(), area.Dy())
@@ -1775,22 +1793,19 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
17751793
17761794 switch m.state {
17771795 case uiOnboarding:
1778− header := uv.NewStyledString(m.header)
1779− header.Draw(scr, layout.header)
1796+ m.drawHeader(scr, layout.header)
17801797
17811798 // NOTE: Onboarding flow will be rendered as dialogs below, but
17821799 // positioned at the bottom left of the screen.
17831800
17841801 case uiInitialize:
1785− header := uv.NewStyledString(m.header)
1786− header.Draw(scr, layout.header)
1802+ m.drawHeader(scr, layout.header)
17871803
17881804 main := uv.NewStyledString(m.initializeView())
17891805 main.Draw(scr, layout.main)
17901806
17911807 case uiLanding:
1792− header := uv.NewStyledString(m.header)
1793− header.Draw(scr, layout.header)
1808+ m.drawHeader(scr, layout.header)
17941809 main := uv.NewStyledString(m.landingView())
17951810 main.Draw(scr, layout.main)
17961811
@@ -1799,8 +1814,7 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
17991814
18001815 case uiChat:
18011816 if m.isCompact {
1802− header := uv.NewStyledString(m.header)
1803− header.Draw(scr, layout.header)
1817+ m.drawHeader(scr, layout.header)
18041818 } else {
18051819 m.drawSidebar(scr, layout.sidebar)
18061820 }
@@ -2177,14 +2191,9 @@ func (m *UI) updateSize() {
21772191
21782192 // Handle different app states
21792193 switch m.state {
2180− case uiOnboarding, uiInitialize, uiLanding:
2181− m.renderHeader(false, m.layout.header.Dx())
2182−
21832194 case uiChat:
2184− if m.isCompact {
2185− m.renderHeader(true, m.layout.header.Dx())
2186− } else {
2187− m.renderSidebarLogo(m.layout.sidebar.Dx())
2195+ if !m.isCompact {
2196+ m.cacheSidebarLogo(m.layout.sidebar.Dx())
21882197 }
21892198 }
21902199}
@@ -2590,18 +2599,8 @@ func (m *UI) renderEditorView(width int) string {
25902599 )
25912600}
25922601
2593−// renderHeader renders and caches the header logo at the specified width.
2594−func (m *UI) renderHeader(compact bool, width int) {
2595− if compact && m.session != nil && m.com.App != nil {
2596− m.header = renderCompactHeader(m.com, m.session, m.com.App.LSPClients, m.detailsOpen, width)
2597− } else {
2598− m.header = renderLogo(m.com.Styles, compact, width)
2599− }
2600−}
2601−
2602−// renderSidebarLogo renders and caches the sidebar logo at the specified
2603−// width.
2604−func (m *UI) renderSidebarLogo(width int) {
2602+// cacheSidebarLogo renders and caches the sidebar logo at the specified width.
2603+func (m *UI) cacheSidebarLogo(width int) {
26052604 m.sidebarLogo = renderLogo(m.com.Styles, true, width)
26062605}
26072606