on main help sign in
fix(ui): mark estimated context usage
commit2736e487cf511a686daf691d9135f9491d4ac865 authorGreg Slepak <contact@taoeffect.com> dateTue 12 May 2026, 17:34 (5 months ago) parent83d2abb
9 files changed+74−20
internal/agent/agent.go+5 internal/agent/usage_fallback_test.go+2 internal/session/session.go+3 internal/ui/common/elements.go+11−6 internal/ui/common/elements_test.go+32 internal/ui/model/header.go+5−1 internal/ui/model/sidebar.go+4−3 internal/ui/styles/quickstyle.go+1 internal/ui/styles/styles.go+11−10
internal/agent/agent.go+5
@@ -762,6 +762,7 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string, opts fan
762762 currentSession.SummaryMessageID = summaryMessage.ID
763763 currentSession.CompletionTokens = summaryCompletionTokens(usage, summaryMessage)
764764 currentSession.PromptTokens = 0
765+ currentSession.EstimatedUsage = usageIsZero(usage)
765766 _, err = a.sessions.Save(genCtx, currentSession)
766767 if err != nil {
767768 return err
@@ -1139,6 +1140,10 @@ func (a *sessionAgent) openrouterCost(metadata fantasy.ProviderMetadata) *float6
11391140}
11401141
11411142func (a *sessionAgent) updateSessionUsage(model Model, session *session.Session, usage fantasy.Usage, overrideCost *float64, estimated bool) {
1143+ if !usageIsZero(usage) {
1144+ session.EstimatedUsage = estimated
1145+ }
1146+
11421147 modelConfig := model.CatwalkCfg
11431148 cost := modelConfig.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
11441149 modelConfig.CostPer1MOutCached/1e6*float64(usage.CacheReadTokens) +
internal/agent/usage_fallback_test.go+2
@@ -212,6 +212,7 @@ func TestUpdateSessionUsageSkipsEstimatedCost(t *testing.T) {
212212 require.Equal(t, 1.25, currentSession.Cost)
213213 require.Equal(t, int64(1000), currentSession.PromptTokens)
214214 require.Equal(t, int64(2000), currentSession.CompletionTokens)
215+ require.True(t, currentSession.EstimatedUsage)
215216}
216217
217218func TestUpdateSessionUsageKeepsCountersForZeroUsage(t *testing.T) {
@@ -334,4 +335,5 @@ func TestUpdateSessionUsageAddsProviderCost(t *testing.T) {
334335 require.Equal(t, 1.3, currentSession.Cost)
335336 require.Equal(t, int64(1000), currentSession.PromptTokens)
336337 require.Equal(t, int64(2000), currentSession.CompletionTokens)
338+ require.False(t, currentSession.EstimatedUsage)
337339}
internal/session/session.go+3
@@ -53,6 +53,7 @@ type Session struct {
5353 MessageCount int64
5454 PromptTokens int64
5555 CompletionTokens int64
56+ EstimatedUsage bool
5657 SummaryMessageID string
5758 Cost float64
5859 Todos []Todo
@@ -199,7 +200,9 @@ func (s *service) Save(ctx context.Context, session Session) (Session, error) {
199200 if err != nil {
200201 return Session{}, err
201202 }
203+ estimatedUsage := session.EstimatedUsage
202204 session = s.fromDBItem(dbSession)
205+ session.EstimatedUsage = estimatedUsage
203206 s.Publish(pubsub.UpdatedEvent, session)
204207 return session, nil
205208}
internal/ui/common/elements.go+11−6
@@ -33,9 +33,10 @@ func FormatReasoningEffort(effort string) string {
3333
3434// ModelContextInfo contains token usage and cost information for a model.
3535type ModelContextInfo struct {
36− ContextUsed int64
37− ModelContext int64
38− Cost float64
36+ ContextUsed int64
37+ ModelContext int64
38+ Cost float64
39+ EstimatedUsage bool
3940}
4041
4142// ModelInfo renders model information including name, provider, reasoning
@@ -74,7 +75,7 @@ func ModelInfo(t *styles.Styles, modelName, providerName, reasoningInfo string,
7475 }
7576
7677 if context != nil {
77− formattedInfo := formatTokensAndCost(t, context.ContextUsed, context.ModelContext, context.Cost)
78+ formattedInfo := formatTokensAndCost(t, context.ContextUsed, context.ModelContext, context.Cost, context.EstimatedUsage)
7879 parts = append(parts, lipgloss.NewStyle().PaddingLeft(2).Render(formattedInfo))
7980 }
8081
@@ -92,7 +93,7 @@ func ModelInfo(t *styles.Styles, modelName, providerName, reasoningInfo string,
9293
9394// formatTokensAndCost formats token usage and cost with appropriate units
9495// (K/M) and percentage of context window.
95−func formatTokensAndCost(t *styles.Styles, tokens, contextWindow int64, cost float64) string {
96+func formatTokensAndCost(t *styles.Styles, tokens, contextWindow int64, cost float64, estimated bool) string {
9697 var formattedTokens string
9798 switch {
9899 case tokens >= 1_000_000:
@@ -115,7 +116,11 @@ func formatTokensAndCost(t *styles.Styles, tokens, contextWindow int64, cost flo
115116 formattedCost := t.ModelInfo.Cost.Render(fmt.Sprintf("$%.2f", cost))
116117
117118 formattedTokens = t.ModelInfo.TokenCount.Render(fmt.Sprintf("(%s)", formattedTokens))
118− formattedPercentage := t.ModelInfo.TokenPercentage.Render(fmt.Sprintf("%d%%", int(percentage)))
119+ percentageText := fmt.Sprintf("%d%%", int(percentage))
120+ if estimated {
121+ percentageText = t.ModelInfo.EstimatedUsagePrefix.Render("~") + percentageText
122+ }
123+ formattedPercentage := t.ModelInfo.TokenPercentage.Render(percentageText)
119124 formattedTokens = fmt.Sprintf("%s %s", formattedPercentage, formattedTokens)
120125 if percentage > 80 {
121126 formattedTokens = fmt.Sprintf("%s %s", styles.LSPWarningIcon, formattedTokens)
internal/ui/common/elements_test.gonew file+32
@@ -0,0 +1,32 @@
1+package common
2+
3+import (
4+ "testing"
5+
6+ "github.com/charmbracelet/crush/internal/ui/styles"
7+ "github.com/charmbracelet/x/ansi"
8+ "github.com/stretchr/testify/require"
9+)
10+
11+func TestFormatTokensAndCostPrefixesEstimatedUsage(t *testing.T) {
12+ t.Parallel()
13+
14+ sty := styles.CharmtonePantera()
15+
16+ actual := ansi.Strip(formatTokensAndCost(&sty, 120, 1000, 0, true))
17+
18+ require.Contains(t, actual, "~12%")
19+ require.Contains(t, actual, "(120)")
20+ require.Contains(t, actual, "$0.00")
21+}
22+
23+func TestFormatTokensAndCostOmitsEstimatedPrefix(t *testing.T) {
24+ t.Parallel()
25+
26+ sty := styles.CharmtonePantera()
27+
28+ actual := ansi.Strip(formatTokensAndCost(&sty, 120, 1000, 0, false))
29+
30+ require.Contains(t, actual, "12%")
31+ require.NotContains(t, actual, "~12%")
32+}
internal/ui/model/header.go+5−1
@@ -148,7 +148,11 @@ func renderHeaderDetails(
148148 model := com.Config().GetModelByType(agentCfg.Model)
149149 if model != nil && model.ContextWindow > 0 {
150150 percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
151− formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage)))
151+ percentageText := fmt.Sprintf("%d%%", int(percentage))
152+ if session.EstimatedUsage {
153+ percentageText = t.ModelInfo.EstimatedUsagePrefix.Render("~") + percentageText
154+ }
155+ formattedPercentage := t.Header.Percentage.Render(percentageText)
152156 parts = append(parts, formattedPercentage)
153157 }
154158
internal/ui/model/sidebar.go+4−3
@@ -44,9 +44,10 @@ func (m *UI) modelInfo(width int) string {
4444 var modelContext *common.ModelContextInfo
4545 if model != nil && m.session != nil {
4646 modelContext = &common.ModelContextInfo{
47− ContextUsed: m.session.CompletionTokens + m.session.PromptTokens,
48− Cost: m.session.Cost,
49− ModelContext: model.CatwalkCfg.ContextWindow,
47+ ContextUsed: m.session.CompletionTokens + m.session.PromptTokens,
48+ Cost: m.session.Cost,
49+ ModelContext: model.CatwalkCfg.ContextWindow,
50+ EstimatedUsage: m.session.EstimatedUsage,
5051 }
5152 }
5253 var modelName string
internal/ui/styles/quickstyle.go+1
@@ -763,6 +763,7 @@ func quickStyle(o quickStyleOpts) Styles {
763763 s.ModelInfo.Reasoning = lipgloss.NewStyle().Foreground(o.fgMostSubtle).PaddingLeft(2)
764764 s.ModelInfo.TokenCount = lipgloss.NewStyle().Foreground(o.fgMostSubtle)
765765 s.ModelInfo.TokenPercentage = lipgloss.NewStyle().Foreground(o.fgMoreSubtle)
766+ s.ModelInfo.EstimatedUsagePrefix = lipgloss.NewStyle().Foreground(o.fgBase)
766767 s.ModelInfo.Cost = lipgloss.NewStyle().Foreground(o.fgMoreSubtle)
767768 s.ModelInfo.HypercreditIcon = lipgloss.NewStyle().Foreground(charmtone.Dolly)
768769 s.ModelInfo.HypercreditText = lipgloss.NewStyle().Foreground(o.fgMoreSubtle)
internal/ui/styles/styles.go+11−10
@@ -183,16 +183,17 @@ type Styles struct {
183183
184184 // ModelInfo (model name, provider, reasoning, token/cost summary)
185185 ModelInfo struct {
186− Icon lipgloss.Style // Model icon (◇)
187− Name lipgloss.Style // Model name text
188− Provider lipgloss.Style // "via <provider>" text
189− ProviderFallback lipgloss.Style // Provider on its own second line
190− Reasoning lipgloss.Style // Reasoning effort text
191− TokenCount lipgloss.Style // "(42K)" token count
192− TokenPercentage lipgloss.Style // "42%" percent of context window
193− Cost lipgloss.Style // "$0.42" cost readout
194− HypercreditIcon lipgloss.Style // Hypercredit icon (◆)
195− HypercreditText lipgloss.Style // Remaining Hypercredits text
186+ Icon lipgloss.Style // Model icon (◇)
187+ Name lipgloss.Style // Model name text
188+ Provider lipgloss.Style // "via <provider>" text
189+ ProviderFallback lipgloss.Style // Provider on its own second line
190+ Reasoning lipgloss.Style // Reasoning effort text
191+ TokenCount lipgloss.Style // "(42K)" token count
192+ TokenPercentage lipgloss.Style // "42%" percent of context window
193+ EstimatedUsagePrefix lipgloss.Style // "~" prefix for estimated usage
194+ Cost lipgloss.Style // "$0.42" cost readout
195+ HypercreditIcon lipgloss.Style // Hypercredit icon (◆)
196+ HypercreditText lipgloss.Style // Remaining Hypercredits text
196197 }
197198
198199 // Resource styles the LSP/MCP/skills sidebar lists: their heading,