diff --git a/cmd/statusline.go b/cmd/statusline.go index ffdf6d0..2e4f5a0 100644 --- a/cmd/statusline.go +++ b/cmd/statusline.go @@ -11,8 +11,6 @@ import ( "strings" "github.com/spf13/cobra" - - "github.com/RandomCodeSpace/ctm/internal/claude" ) func init() { @@ -24,17 +22,15 @@ func init() { // we print a three-line display on stdout. Hidden because it's an // internal hook, not a user-facing command. // -// Output layout (3 lines): +// Output layout (2 lines): // // Line 1: · (project shown as a plain path) -// Line 2: c 25% (437k) w 40% h 10% (context % + tokens + rate limits) -// Line 3: ↑ 117k ↓ 434k (cumulative session input / output) +// Line 2: ctx 25% w 40% h 10% (context % + rate limits) // -// Cache_read (⚡) was dropped from the status because its magnitude is -// already captured in the context-tokens parenthesis and Claude Code's -// own focus-mode overlay duplicates the information. Weekly / 5-hour -// rate limits share line 2 with context because they're all -// percentages; tokens share line 3 because both are cumulative ints. +// Cache_read (⚡) was dropped because its magnitude is already captured +// in the context-tokens parenthesis and Claude Code's own focus-mode +// overlay duplicates the information. Weekly / 5-hour rate limits +// share line 2 with context because they're all percentages. var statuslineCmd = &cobra.Command{ Use: "statusline", Short: "Internal statusLine renderer — reads JSON on stdin (hidden)", @@ -138,9 +134,6 @@ const ( cMagenta = "\x1b[1;38;5;220m" // weekly bar cYellow = "\x1b[1;38;5;208m" // 5-hour bar cHdrModel = "\x1b[1;97m" - cTokIn = "\x1b[1;38;5;33m" - cTokOut = "\x1b[1;38;5;37m" - cDimGray = "\x1b[90m" ) func renderStatusline(in *statuslineInput) string { @@ -153,9 +146,6 @@ func renderStatusline(in *statuslineInput) string { if mid != "" { lines = append(lines, mid) } - if s := buildTokenLine(in); s != "" { - lines = append(lines, s) - } return strings.Join(lines, "\n") } @@ -201,21 +191,7 @@ func buildHeader(in *statuslineInput) string { } } -// readEffortLevel is a cmd-package-local wrapper so other renderers can -// pick up the current effort level without duplicating the path dance. -// Silent on every error path — effort is a nice-to-have, not critical. -func readEffortLevel() string { - p, err := claude.SettingsJSONPath() - if err != nil { - return "" - } - return claude.ReadEffortLevel(p) -} - -// buildContextLine builds the `c % ()` segment of line 2. -// The context-window-used percentage is the primary signal; the -// parenthesised token sum (input + cache_creation + cache_read, per -// Claude Code's input-only formula) is a secondary concrete number. +// buildContextLine builds the `ctx %` segment of line 2. // Returns "" when used_percentage is absent. func buildContextLine(in *statuslineInput) string { used := in.ContextWindow.UsedPercentage @@ -223,44 +199,7 @@ func buildContextLine(in *statuslineInput) string { return "" } usedPct := int(math.Round(*used)) - entry := fmt.Sprintf("%sc %d%%%s", cCyan, usedPct, cReset) - if ctx := contextTokens(in); ctx > 0 { - entry += fmt.Sprintf(" %s(%s)%s", cDimGray, fmtTokens(ctx), cReset) - } - return entry -} - -// buildTokenLine renders the cumulative session token totals: `↑ ` -// and `↓ `. cache_read (⚡) used to live here too; it was dropped -// from the statusline — the token magnitude is already visible as the -// parenthesised number on the context line and Claude Code's focus-mode -// overlay renders its own cache indicator. -func buildTokenLine(in *statuslineInput) string { - var parts []string - add := func(glyph rune, color string, n *int64) { - if n == nil || *n <= 0 { - return - } - parts = append(parts, fmt.Sprintf("%s%c%s %s%s%s", - color, glyph, cReset, cDimGray, fmtTokens(*n), cReset)) - } - add('↑', cTokIn, in.ContextWindow.TotalInputTokens) - add('↓', cTokOut, in.ContextWindow.TotalOutputTokens) - line := strings.Join(parts, " ") - - // Tack the current effort level onto the last line. Sourced from - // ~/.claude/settings.json via readEffortLevel — not in Claude - // Code's statusLine payload. Dim-gray so it reads as secondary - // info next to the token counts. Only appended when at least one - // token is present so a truly empty payload doesn't render a - // lone "· xhigh" orphan. - if line == "" { - return "" - } - if effort := readEffortLevel(); effort != "" { - line += fmt.Sprintf(" %s%s%s", cDimGray, effort, cReset) - } - return line + return fmt.Sprintf("%sctx %d%%%s", cCyan, usedPct, cReset) } // buildRateLimitLine renders `w %` and `h %` for weekly and diff --git a/cmd/statusline_test.go b/cmd/statusline_test.go index 874a6ea..03d6e08 100644 --- a/cmd/statusline_test.go +++ b/cmd/statusline_test.go @@ -22,36 +22,28 @@ func TestRenderStatuslineFullPayload(t *testing.T) { out := renderStatusline(in) lines := strings.Split(out, "\n") - if len(lines) != 3 { - t.Fatalf("expected 3 lines, got %d:\n%s", len(lines), out) + if len(lines) != 2 { + t.Fatalf("expected 2 lines, got %d:\n%s", len(lines), out) } - // Line 0 — Header: full model name (minus the redundant "Claude " - // prefix) plus the project tail. if !strings.Contains(lines[0], "Sonnet 4.5 (1M)") { t.Errorf("header missing full model name: %q", lines[0]) } if !strings.Contains(lines[0], "ctm-statusline-fake") { t.Errorf("header missing project tail: %q", lines[0]) } - // Line 1 — Context + rate limits share one line now: c / w / h. - for _, want := range []string{"c", "25%", "w", "40%", "h", "10%"} { + for _, want := range []string{"ctx", "25%", "w", "40%", "h", "10%"} { if !strings.Contains(lines[1], want) { t.Errorf("line 2 missing %q: %q", want, lines[1]) } } - // ⚡ should NOT appear anywhere — cache was dropped as a separate - // statusline entry. (The cache_read value may still contribute to - // the parenthesised context-tokens sum, which is expected.) - if strings.Contains(out, "⚡") { - t.Errorf("cache glyph ⚡ should have been removed:\n%s", out) + if strings.Contains(lines[1], "(") || strings.Contains(lines[1], ")") { + t.Errorf("line 2 should not contain token-count parens: %q", lines[1]) } - // Line 2 — Tokens: input + output only. - for _, want := range []string{"↑", "↓", "12.3k", "6.8k"} { - if !strings.Contains(lines[2], want) { - t.Errorf("token line missing %q: %q", want, lines[2]) + for _, banned := range []string{"⚡", "↑", "↓"} { + if strings.Contains(out, banned) { + t.Errorf("dropped glyph %q should not appear:\n%s", banned, out) } } - // No bar runes should appear anywhere. for _, bar := range []string{"━", "─"} { if strings.Contains(out, bar) { t.Errorf("unexpected bar rune %q in output:\n%s", bar, out) @@ -122,44 +114,6 @@ func TestContextTokens_AllNilReturnsZero(t *testing.T) { } } -func TestRenderStatuslineShowsContextTokens(t *testing.T) { - in := &statuslineInput{} - in.Model.DisplayName = "Claude Sonnet 4.5" - in.ContextWindow.UsedPercentage = floatPtr(42) - in.ContextWindow.CurrentUsage.InputTokens = intPtr(12000) - in.ContextWindow.CurrentUsage.CacheCreationInputTokens = intPtr(8000) - in.ContextWindow.CurrentUsage.CacheReadInputTokens = intPtr(417270) - // Sum = 437 270 → formats as "437.3k" - - out := renderStatusline(in) - lines := strings.Split(out, "\n") - if len(lines) < 2 { - t.Fatalf("expected at least 2 lines, got:\n%s", out) - } - if !strings.Contains(lines[1], "437.3k") { - t.Errorf("line 2 missing context token count: %q", lines[1]) - } - if !strings.Contains(lines[1], "42%") { - t.Errorf("line 2 missing context %%: %q", lines[1]) - } -} - -func TestRenderStatuslineOmitsContextTokensWhenZero(t *testing.T) { - in := &statuslineInput{} - in.Model.DisplayName = "Claude Sonnet 4.5" - in.ContextWindow.UsedPercentage = floatPtr(0) - // current_usage absent — contextTokens = 0 - - out := renderStatusline(in) - lines := strings.Split(out, "\n") - if len(lines) < 2 { - t.Fatalf("expected at least 2 lines, got:\n%s", out) - } - if strings.Contains(lines[1], "(") { - t.Errorf("line 2 unexpectedly includes a token-count paren: %q", lines[1]) - } -} - func TestFmtTokens(t *testing.T) { cases := map[int64]string{ 0: "0",