Skip to content

feat: statusline usage percentages and responsive layout#37

Merged
barkain merged 13 commits into
mainfrom
feature/statusline-usage-percentages
Apr 3, 2026
Merged

feat: statusline usage percentages and responsive layout#37
barkain merged 13 commits into
mainfrom
feature/statusline-usage-percentages

Conversation

@barkain

@barkain barkain commented Apr 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add 5h and weekly API usage percentages to statusline row 2, read from Claude Code's rate_limits JSON input (zero API calls)
  • Remove sparkline/cycle time graph visualization (keep duration text)
  • Remove session cost display
  • Responsive layout with progressive truncation for narrow terminals
  • Shorten context bar from 20 to 10 characters

Changes

  • Usage percentages: Read rate_limits.five_hour.used_percentage and rate_limits.seven_day.used_percentage from statusLine JSON stdin. Color-coded: green (<50%), yellow (50-75%), red (>75%)
  • Removed sparkline: generate_sparkline() and get_duration_history() removed. Duration text (e.g., "24s") preserved
  • Responsive layout: Terminal width detection with 4-tier progressive compaction. Dir truncated to 25 chars, branch to 20 chars
  • Simplified: Originally tried OAuth usage API but it's permanently rate-limited (~5 calls then 429). Switched to reading from stdin which is the correct approach

Row 2 layout

context bar | 5h X% · weekly X% | $cost | duration

Test plan

  • Verify usage percentages display correctly in normal terminal
  • Verify row 2 shows in narrow/split terminals
  • Verify dir and branch truncate with ellipsis
  • Verify color coding thresholds work

🤖 Generated with Claude Code

@qodo-code-review

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Add usage percentages and responsive statusline layout

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add 5h and weekly API usage percentages to statusline row 2 from rate_limits JSON input
• Implement responsive layout with progressive truncation for narrow terminals (4-tier compaction)
• Remove sparkline visualization and session cost display, keep duration text only
• Shorten context bar from 20 to 10 characters and simplify git branch display
• Add terminal width detection and dynamic row 2 formatting based on available space
Diagram
flowchart LR
  A["statusLine JSON input"] -->|rate_limits| B["format_usage_percentages"]
  B -->|5h/weekly %| C["Row 2 layout"]
  D["Terminal width"] -->|responsive| C
  C -->|≥100 cols| E["Full: context | usage | cost | duration"]
  C -->|80-99 cols| F["Medium: context | usage | cost"]
  C -->|60-79 cols| G["Compact: context | compact usage"]
  C -->|<60 cols| H["Minimal: percentage | compact usage"]
  I["Git branch"] -->|truncate_str| J["Row 1 progressive shortening"]
  K["Directory path"] -->|truncate_str| J
Loading

Grey Divider

File Changes

1. scripts/statusline.py ✨ Enhancement +284/-116

Responsive statusline with usage percentages and progressive truncation

• Added format_usage_percentages() to display 5h and 7-day API usage percentages with color coding
 (green <50%, yellow 50-75%, red >75%)
• Added get_terminal_width() for reliable terminal width detection with fallback to 120 columns
• Added truncate_str() utility to intelligently truncate strings with ellipsis (showing start and
 end)
• Implemented 4-tier responsive row 2 layout based on terminal width (full/medium/compact/minimal
 modes)
• Removed generate_sparkline() and get_duration_history() functions; kept duration text only
• Modified get_git_branch() to return raw branch name without emoji/decorators for flexible
 formatting
• Updated cost caching to store raw float values (daily_cost_val, weekly_cost_val) for
 percentage calculations
• Enhanced fetch_costs_raw() and background refresh script to fetch both daily and weekly cost
 data
• Simplified row 1 formatting with progressive truncation of directory and branch based on terminal
 width
• Removed session cost display; now shows only daily cost with money bag emoji

scripts/statusline.py


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. Narrow width overridden🐞 Bug ≡ Correctness
Description
get_terminal_width() forces any detected width <40 to 120 columns, so genuinely narrow terminals
won’t trigger compact/minimal layouts and the statusline will wrap/overflow instead of compacting.
Code

scripts/statusline.py[R256-271]

+def get_terminal_width() -> int:
+    """Get terminal width, defaulting to 120 if detection fails.
+
+    In Claude Code's statusline context, shutil.get_terminal_size() often
+    returns unreasonably small values because it's not a real TTY.
+    We treat anything below 40 columns as unreliable and default to 120.
+    """
+    try:
+        import shutil
+
+        width = shutil.get_terminal_size(fallback=(120, 24)).columns
+        # In non-TTY contexts (like Claude Code statusline), the detected
+        # width can be 0 or very small. Use a conservative default instead.
+        if width < 40:
+            return 120
+        return width
Evidence
The new width detection treats any width under 40 as “unreliable” and returns 120, but main() uses
that width to choose between full/medium/compact/minimal row2 layouts; therefore real <40-column
terminals will never enter the compact/minimal branches.

scripts/statusline.py[256-273]
scripts/statusline.py[781-890]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`get_terminal_width()` overrides any detected width `< 40` to `120`, which prevents the new responsive logic from ever activating in genuinely narrow terminals. This causes row wrapping/overflow and contradicts the “progressive compaction for narrow terminals” behavior.
### Issue Context
The responsive thresholds in `main()` (`>=100/80/60/else`) depend entirely on `term_width`, but `get_terminal_width()` currently prevents `term_width` from ever being `< 40`.
### Fix Focus Areas
- scripts/statusline.py[256-273]
- scripts/statusline.py[781-890]
### Suggested approach
- Only fall back to 120 when width detection fails (e.g., returns 0/None/raises), not when it returns a small positive width.
- If you still need a Claude-Code-specific workaround, gate it behind a detectable signal (e.g., `sys.stdin.isatty()`/`sys.stdout.isatty()`, an env var override, or `COLUMNS`), rather than a hard `<40` cutoff.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. 50% usage miscolored🐞 Bug ≡ Correctness
Description
format_usage_percentages() treats exactly 50% as green because the yellow threshold is implemented
as pct > 50. This contradicts the PR’s stated thresholds (yellow should include 50%), so boundary
values will display the wrong severity color.
Code

scripts/statusline.py[R666-671]

+    def color_for_pct(pct: float) -> str:
+        if pct > 75:
+            return "\033[31m"  # Red
+        elif pct > 50:
+            return "\033[33m"  # Yellow
+        return "\033[32m"  # Green
Evidence
The PR description states the intended thresholds (green <50%, yellow 50–75%, red >75%), but the
code uses strict > comparisons, making 50.0 fall into the green case.

scripts/statusline.py[666-671]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`format_usage_percentages()` currently colors exactly 50% as green because the yellow branch uses `pct > 50`. The PR intent says yellow should cover 50–75.
### Issue Context
This affects the new 5h/weekly usage display color-coding at the boundary value.
### Fix Focus Areas
- scripts/statusline.py[666-671]
### Suggested change
Update the comparison to include the boundary, e.g.:
- `elif pct >= 50:` for yellow
- keep red as `pct > 75` per the stated threshold

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Context percent not clamped🐞 Bug ≡ Correctness
Description
calculate_context_usage() can return a raw context percentage greater than 100, and the minimal
layout prints it directly as ctx_pct. This can display nonsensical values (e.g., 135%) while the
progress-bar fill is clamped to 100%, creating inconsistent/misleading output.
Code

scripts/statusline.py[R847-853]

+        # Minimal: just percentage + compact usage (drop bar, emoji, cost, duration)
+        compact_usage = format_usage_percentages(
+            five_hour_pct, seven_day_pct, compact=True
+        )
+        # Reuse raw percentage from calculate_context_usage() (no re-read)
+        ctx_pct = f"{ctx_raw_pct:.0f}%"
+        row2_parts = [ctx_pct, compact_usage]
Evidence
The raw percentage is computed as total_input * 100 / max_context and returned as ctx_raw_pct
without clamping; the minimal row2 layout formats it directly. Separately, the bar fill clamps
usage_rate to 0–100 for rendering, so the numeric percentage can disagree with the bar’s
semantics.

scripts/statusline.py[199-213]
scripts/statusline.py[44-47]
scripts/statusline.py[847-853]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In the minimal layout, `ctx_raw_pct` is displayed directly. Since it is computed as `total_input * 100 / max_context` and not bounded, it can exceed 100%, producing misleading output.
### Issue Context
`create_progress_bar()` clamps the bar fill to 0–100, but the minimal layout prints the raw value; this inconsistency is user-visible only on narrow terminals.
### Fix Focus Areas
- scripts/statusline.py[199-213]
- scripts/statusline.py[847-853]
### Suggested change
Either:
1) Clamp at the source:
- return `min(100.0, max(0.0, usage_rate))` as the second tuple value from `calculate_context_usage()`.
Or:
2) Clamp at display time in the minimal layout:
- `ctx_pct_val = max(0.0, min(100.0, ctx_raw_pct))`
- `ctx_pct = f"{ctx_pct_val:.0f}%"`
(Option 1 keeps all downstream uses consistent.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Double session file read🐞 Bug ➹ Performance
Description
When term_width <60, main() re-reads and stores the entire session JSONL to recompute ctx_pct even
though calculate_context_usage() already scanned the session file earlier in the same run, doubling
I/O and memory in the narrow-terminal path.
Code

scripts/statusline.py[R845-889]

+    else:
+        # Minimal: just percentage + compact usage (drop bar, emoji, cost, duration)
+        # Extract percentage from context_info by recalculating
+        compact_usage = format_usage_percentages(
+            five_hour_pct, seven_day_pct, compact=True
+        )
+        # Build a minimal context display (just percentage, no bar)
+        session_id = input_data.get("session_id", "")
+        max_ctx = 200000
+        if "1M" in raw_model:
+            max_ctx = 1_000_000
+        # Try to get actual percentage from session data
+        ctx_pct = "0%"
+        if session_id:
+            session_file = find_session_file(
+                session_id,
+                input_data.get("cwd")
+                or input_data.get("workspace", {}).get("current_dir", ""),
+            )
+            if session_file:
+                try:
+                    last_reset_line = 0
+                    lines_list: list[str] = []
+                    with open(session_file, "r", encoding="utf-8") as f:
+                        for i, line in enumerate(f, 1):
+                            lines_list.append(line)
+                            if '"/clear"' in line or '"/compact"' in line:
+                                last_reset_line = i
+                    for line in lines_list[last_reset_line:]:
+                        try:
+                            entry = json.loads(line)
+                            usage_data = entry.get("message", {}).get("usage", {})
+                            if usage_data:
+                                total_in = (
+                                    usage_data.get("input_tokens", 0)
+                                    + usage_data.get("cache_read_input_tokens", 0)
+                                    + usage_data.get("cache_creation_input_tokens", 0)
+                                )
+                                if total_in > 0:
+                                    ctx_pct = f"{total_in * 100 / max_ctx:.0f}%"
+                        except json.JSONDecodeError:
+                            continue
+                except OSError:
+                    pass
+        row2_parts = [f"{ctx_pct}", compact_usage]
Evidence
calculate_context_usage() reads the session file into a list to compute context usage, and
main() calls it before width-based layout selection. In the minimal-layout branch (`term_width <
60`), the code opens the same session file again and appends every line into another list
(lines_list) to recompute the percentage, duplicating the expensive work.

scripts/statusline.py[129-212]
scripts/statusline.py[749-757]
scripts/statusline.py[845-889]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
For `term_width < 60`, `main()` recomputes context percentage by re-opening and fully reading the session JSONL into memory, even though `calculate_context_usage()` already performed a full scan earlier in the same invocation. This doubles disk I/O and allocations on the narrow-terminal path.
### Issue Context
- `main()` calls `calculate_context_usage(input_data)` before layout selection.
- `calculate_context_usage()` reads the session file into `lines`.
- The minimal branch repeats the full read into `lines_list` and re-parses JSON.
### Fix Focus Areas
- scripts/statusline.py[129-212]
- scripts/statusline.py[749-757]
- scripts/statusline.py[845-889]
### Suggested approach
- Refactor `calculate_context_usage()` to optionally return structured data (e.g., `total_input_tokens`, `max_context`, `usage_rate`) alongside the formatted string, so the minimal branch can reuse it without re-reading.
- Alternatively, avoid calling `calculate_context_usage()` entirely when `term_width < 60` (decide layout first), and compute only what’s needed for that layout.
- Additionally, consider streaming the JSONL rather than storing all lines in memory (track last reset position and last usage entry without accumulating the full file).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

5. Usage docstring incorrect🐞 Bug ⚙ Maintainability
Description
format_usage_percentages() docstring says percentages come from the OAuth usage API, but main()
passes values read from stdin rate_limits, making the documentation misleading.
Code

scripts/statusline.py[R646-658]

+def format_usage_percentages(
+    five_hour_pct: float | None,
+    seven_day_pct: float | None,
+    compact: bool = False,
+) -> str:
+    """Format 5h and weekly usage as colored percentage strings.
+
+    Uses real utilization values from the Anthropic OAuth usage API.
+
+    Args:
+        five_hour_pct: 5-hour utilization percentage (0-100) from API, or None if unavailable.
+        seven_day_pct: 7-day utilization percentage (0-100) from API, or None if unavailable.
+        compact: If True, use shorter labels (e.g., "5h 2%·w 60%").
Evidence
The docstring explicitly claims an OAuth usage API source, while the actual call site extracts
used_percentage from input_data['rate_limits'] (stdin JSON) and passes those values into
format_usage_percentages().

scripts/statusline.py[646-697]
scripts/statusline.py[772-780]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`format_usage_percentages()` documentation claims the values come from the Anthropic OAuth usage API, but the implementation is actually fed from statusline stdin JSON (`rate_limits.*.used_percentage`). This mismatch will confuse future maintainers.
### Issue Context
The PR’s new logic reads `rate_limits` from `input_data` and formats it; there is no API call.
### Fix Focus Areas
- scripts/statusline.py[646-697]
- scripts/statusline.py[772-780]
### Suggested approach
- Update the docstring and parameter descriptions to state the source is `input_data['rate_limits']` (stdin JSON) and define expected types/range (0–100).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread scripts/statusline.py Outdated
@barkain

barkain commented Apr 3, 2026

Copy link
Copy Markdown
Owner Author

/agentic_review

@qodo-code-review

qodo-code-review Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit b1e182e

@barkain
barkain merged commit d96c5c0 into main Apr 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant