From f53402d57eac0590d009611cf892c8957eab15e7 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 4 Feb 2026 12:58:55 +0300 Subject: [PATCH] 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 --- internal/ui/model/header.go | 62 ++++++++++++++++++++++++++++--------- internal/ui/model/ui.go | 57 +++++++++++++++++----------------- 2 files changed, 75 insertions(+), 44 deletions(-) diff --git a/internal/ui/model/header.go b/internal/ui/model/header.go index e01a19143c20e0d3e2c6753b719c28092077ac91..5e704bf6ed8a5f69e224ceeca05d34ad59740789 100644 --- a/internal/ui/model/header.go +++ b/internal/ui/model/header.go @@ -12,6 +12,7 @@ import ( "github.com/charmbracelet/crush/internal/session" "github.com/charmbracelet/crush/internal/ui/common" "github.com/charmbracelet/crush/internal/ui/styles" + uv "github.com/charmbracelet/ultraviolet" "github.com/charmbracelet/x/ansi" ) @@ -22,29 +23,58 @@ const ( rightPadding = 1 ) -// renderCompactHeader renders the compact header for the given session. -func renderCompactHeader( - com *common.Common, +type header struct { + // cached logo and compact logo + logo string + compactLogo string + + com *common.Common + width int + compact bool +} + +// newHeader creates a new header model. +func newHeader(com *common.Common) *header { + h := &header{ + com: com, + } + t := com.Styles + h.compactLogo = t.Header.Charm.Render("Charmâ„¢") + " " + + styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary) + " " + return h +} + +// drawHeader draws the header for the given session. +func (h *header) drawHeader( + scr uv.Screen, + area uv.Rectangle, session *session.Session, - lspClients *csync.Map[string, *lsp.Client], + compact bool, detailsOpen bool, width int, -) string { - if session == nil || session.ID == "" { - return "" +) { + t := h.com.Styles + if width != h.width || compact != h.compact { + h.logo = renderLogo(h.com.Styles, compact, width) } - t := com.Styles + h.width = width + h.compact = compact - var b strings.Builder + if !compact || session == nil || h.com.App == nil { + uv.NewStyledString(h.logo).Draw(scr, area) + return + } + + if session.ID == "" { + return + } - b.WriteString(t.Header.Charm.Render("Charmâ„¢")) - b.WriteString(" ") - b.WriteString(styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary)) - b.WriteString(" ") + var b strings.Builder + b.WriteString(h.compactLogo) availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags - details := renderHeaderDetails(com, session, lspClients, detailsOpen, availDetailWidth) + details := renderHeaderDetails(h.com, session, h.com.App.LSPClients, detailsOpen, availDetailWidth) remainingWidth := width - lipgloss.Width(b.String()) - @@ -61,7 +91,9 @@ func renderCompactHeader( b.WriteString(details) - return t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String()) + view := uv.NewStyledString( + t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String())) + view.Draw(scr, area) } // renderHeaderDetails renders the details section of the header. diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index 28f1a7230308618628c1261c5a3b67fba7432d17..a77f55fb919cdb7c809d86e698c96265797da5b3 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -141,8 +141,7 @@ type UI struct { // isCanceling tracks whether the user has pressed escape once to cancel. isCanceling bool - // header is the last cached header logo - header string + header *header // sendProgressBar instructs the TUI to send progress bar updates to the // terminal. @@ -261,12 +260,15 @@ func New(com *common.Common) *UI { }, ) + header := newHeader(com) + ui := &UI{ com: com, dialog: dialog.NewOverlay(), keyMap: keyMap, textarea: ta, chat: ch, + header: header, completions: comp, attachments: attachments, todoSpinner: todoSpinner, @@ -325,6 +327,10 @@ func (m *UI) Init() tea.Cmd { // setState changes the UI state and focus. func (m *UI) setState(state uiState, focus uiFocusState) { + if state == uiLanding { + // Always turn off compact mode when going to landing + m.isCompact = false + } m.state = state m.focus = focus // Changing the state may change layout, so update it. @@ -1761,6 +1767,18 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { return tea.Batch(cmds...) } +// drawHeader draws the header section of the UI. +func (m *UI) drawHeader(scr uv.Screen, area uv.Rectangle) { + m.header.drawHeader( + scr, + area, + m.session, + m.isCompact, + m.detailsOpen, + m.width, + ) +} + // Draw implements [uv.Drawable] and draws the UI model. func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { layout := m.generateLayout(area.Dx(), area.Dy()) @@ -1775,22 +1793,19 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { switch m.state { case uiOnboarding: - header := uv.NewStyledString(m.header) - header.Draw(scr, layout.header) + m.drawHeader(scr, layout.header) // NOTE: Onboarding flow will be rendered as dialogs below, but // positioned at the bottom left of the screen. case uiInitialize: - header := uv.NewStyledString(m.header) - header.Draw(scr, layout.header) + m.drawHeader(scr, layout.header) main := uv.NewStyledString(m.initializeView()) main.Draw(scr, layout.main) case uiLanding: - header := uv.NewStyledString(m.header) - header.Draw(scr, layout.header) + m.drawHeader(scr, layout.header) main := uv.NewStyledString(m.landingView()) main.Draw(scr, layout.main) @@ -1799,8 +1814,7 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { case uiChat: if m.isCompact { - header := uv.NewStyledString(m.header) - header.Draw(scr, layout.header) + m.drawHeader(scr, layout.header) } else { m.drawSidebar(scr, layout.sidebar) } @@ -2177,14 +2191,9 @@ func (m *UI) updateSize() { // Handle different app states switch m.state { - case uiOnboarding, uiInitialize, uiLanding: - m.renderHeader(false, m.layout.header.Dx()) - case uiChat: - if m.isCompact { - m.renderHeader(true, m.layout.header.Dx()) - } else { - m.renderSidebarLogo(m.layout.sidebar.Dx()) + if !m.isCompact { + m.cacheSidebarLogo(m.layout.sidebar.Dx()) } } } @@ -2590,18 +2599,8 @@ func (m *UI) renderEditorView(width int) string { ) } -// renderHeader renders and caches the header logo at the specified width. -func (m *UI) renderHeader(compact bool, width int) { - if compact && m.session != nil && m.com.App != nil { - m.header = renderCompactHeader(m.com, m.session, m.com.App.LSPClients, m.detailsOpen, width) - } else { - m.header = renderLogo(m.com.Styles, compact, width) - } -} - -// renderSidebarLogo renders and caches the sidebar logo at the specified -// width. -func (m *UI) renderSidebarLogo(width int) { +// cacheSidebarLogo renders and caches the sidebar logo at the specified width. +func (m *UI) cacheSidebarLogo(width int) { m.sidebarLogo = renderLogo(m.com.Styles, true, width) }