|
| 1 | +* [2026-05-14] Stdin-first architecture and UX overhaul :ARCH:FEAT: |
| 2 | + |
| 3 | +** Tension |
| 4 | + |
| 5 | +The statusline was doing too much work independently. Context |
| 6 | +percentage was calculated by parsing the transcript JSONL file — |
| 7 | +reading the last 20 lines, extracting token counts, summing, dividing |
| 8 | +by a window size we also computed ourselves. Quota data came |
| 9 | +exclusively from OAuth API calls we initiated. All of this ran on |
| 10 | +every render (3x per second during debounce). |
| 11 | + |
| 12 | +Meanwhile, Claude Code CLI v2.1.132+ was already sending |
| 13 | +pre-calculated =context_window.used_percentage=, =rate_limits= with |
| 14 | +=resets_at= timestamps, =effort.level=, and =fast_mode= directly in |
| 15 | +the stdin JSON. We were ignoring it and recomputing from scratch. |
| 16 | + |
| 17 | +The real trigger: dumping the raw stdin to debug log and seeing the |
| 18 | +full schema — =context_window=, =rate_limits=, =effort=, =vim.mode=, |
| 19 | +=session_name= — all there, all unused. We were doing 60 lines of |
| 20 | +transcript parsing to get a number the CLI was handing us for free. |
| 21 | + |
| 22 | +** Observation |
| 23 | + |
| 24 | +Raw stdin from CLI v2.1.141 (MAX account): |
| 25 | + |
| 26 | +#+begin_src json |
| 27 | +{ |
| 28 | + "context_window": { |
| 29 | + "used_percentage": 21, |
| 30 | + "context_window_size": 1000000 |
| 31 | + }, |
| 32 | + "rate_limits": { |
| 33 | + "five_hour": { "used_percentage": 4, "resets_at": 1778756400 }, |
| 34 | + "seven_day": { "used_percentage": 4, "resets_at": 1779292800 } |
| 35 | + }, |
| 36 | + "effort": { "level": "high" }, |
| 37 | + "fast_mode": false, |
| 38 | + "thinking": { "enabled": true }, |
| 39 | + "vim": { "mode": "NORMAL" } |
| 40 | +} |
| 41 | +#+end_src |
| 42 | + |
| 43 | +PRO account had =seven_day.used_percentage: 28.000000000000004= — |
| 44 | +floating point noise that our =printf '%.0f'= already handles. |
| 45 | + |
| 46 | +Key format difference: CLI sends =resets_at= as unix epoch seconds. |
| 47 | +Our OAuth API returns it as ISO 8601 strings. The =format_reset_clock= |
| 48 | +and =format_reset_relative= functions needed to handle both. |
| 49 | + |
| 50 | +Code review (linus-code-reviewer agent) found three bugs before |
| 51 | +release: |
| 52 | +1. Color variables defined at line 1222, first used at line 1114 — |
| 53 | + git branch and path had zero ANSI coloring in production |
| 54 | +2. =format_duration(0)= returned "1m" instead of "0m" |
| 55 | +3. Division by zero when =CLAUDE_CONTEXT_LIMIT=0= |
| 56 | + |
| 57 | +CI failed on the model abbreviation test — =opus4.6[1m]= only worked |
| 58 | +when =~/.claude/settings.json= had a configured model. The else branch |
| 59 | +fell back to generic family name "opus[1m]". CI has no settings file. |
| 60 | + |
| 61 | +** Decision |
| 62 | + |
| 63 | +*Stdin as primary, transcript/OAuth as fallback.* Not a full rip-out. |
| 64 | + |
| 65 | +- =context_window.used_percentage= from stdin: use it when present, |
| 66 | + fall back to transcript parsing for CLI < v2.1.132 |
| 67 | +- =rate_limits= from stdin: use when no OAuth cache exists (covers |
| 68 | + first-message display without an API call) |
| 69 | +- OAuth API: still required for extra_usage, prepaid balance, user |
| 70 | + profile/tier — none of these are in stdin |
| 71 | +- Kept =get_context_limit()= and transcript parsing as dead-path |
| 72 | + fallback; ~30 lines of dormant code for backward compat |
| 73 | + |
| 74 | +Rejected alternatives: |
| 75 | +- Rip out transcript parsing entirely: breaks users on older CLI |
| 76 | +- Rip out OAuth fetching: loses extra_usage, tier display, prepaid |
| 77 | + balance — the features that differentiate us |
| 78 | +- Prefer stdin rate_limits over OAuth cache: loses extra_usage data |
| 79 | + that comes in the same OAuth response |
| 80 | + |
| 81 | +*Effort/fast mode display.* Added next to model text: |
| 82 | +- =opus4.6[1m] max= — effort is max |
| 83 | +- =opus4.6[1m] fast= — fast mode enabled |
| 84 | +- Hidden when effort is "high" (the default) — no noise |
| 85 | + |
| 86 | +*--extra display gating.* New =--extra= flag: |
| 87 | +- =always= (default) — backward compatible |
| 88 | +- =on-limit= — show extra only when 5h >= 80% or 7d >= 70% |
| 89 | +- =off= — never show |
| 90 | +- =developer= theme defaults to =on-limit=, =minimal= to =off= |
| 91 | + |
| 92 | +*Color hierarchy.* Principle: color encodes urgency, not category. |
| 93 | +- Path demoted to dim cyan (was normal cyan — same weight as model) |
| 94 | +- Git dirty=yellow (pops), clean=dim yellow (recedes) |
| 95 | +- Time to plain dim (was dim cyan — unnecessary color) |
| 96 | +- Tier label gets semantic color: MAX=green, PRO=cyan |
| 97 | + |
| 98 | +*Versioning.* Team suggested matching CLI version (2.1.141). Rejected: |
| 99 | +creates false coupling. We keep our own semver. Document tested CLI |
| 100 | +version in CHANGELOG. |
| 101 | + |
| 102 | +** Tradeoff |
| 103 | + |
| 104 | +- Transcript fallback is dead code for v2.1.132+ users. It adds ~30 |
| 105 | + lines that will never execute. The alternative (ripping it out) would |
| 106 | + break anyone on an older CLI. |
| 107 | +- OAuth is still needed for the premium features (extra_usage, tier, |
| 108 | + prepaid). If Anthropic adds these to stdin in a future version, we |
| 109 | + can simplify further. Until then, two data paths coexist. |
| 110 | +- The =resets_at= format split (epoch vs ISO) means our reset |
| 111 | + formatting functions have a type-check branch. Small cost, but it's |
| 112 | + the kind of dual-format handling that accumulates. |
| 113 | +- =session_name= is available in stdin but not displayed. Could replace |
| 114 | + or supplement project path. Deferred — low urgency, nonzero design |
| 115 | + work. |
| 116 | +- =vim.mode= available but not displayed — built-in already shows it. |
| 117 | + =thinking.enabled= available but always true for current models. |
| 118 | + |
| 119 | +** Next |
| 120 | + |
| 121 | +The live wire is the OAuth/stdin data merge. Right now, if the OAuth |
| 122 | +cache exists, it wins entirely (it has extra_usage). If it doesn't, |
| 123 | +stdin rate_limits are used. But the OAuth cache can be up to 5 minutes |
| 124 | +stale while stdin is always fresh. The right architecture is: stdin for |
| 125 | +quota percentages (always freshest), OAuth only for extra_usage and |
| 126 | +profile. That requires splitting =build_usage_display= to accept mixed |
| 127 | +sources. Not urgent — the staleness window is small — but it's the |
| 128 | +next structural improvement. |
| 129 | + |
| 130 | +** Artifacts |
| 131 | + |
| 132 | +Committed (v0.4.0): |
| 133 | +- =statusline.sh=: +548/-187 lines across extra_usage, color hierarchy, |
| 134 | + --extra flag, bug fixes, dead code removal |
| 135 | +- =t/statusline.bats=: 73 tests (was 0 before this cycle) |
| 136 | +- =t/install.bats=: 12 tests |
| 137 | +- =.github/workflows/test.yml=: CI on push/PR |
| 138 | +- =install.sh=: one-liner installer |
| 139 | +- =README.md=: full rewrite |
| 140 | +- =CHANGELOG.md=: v0.3.0 and v0.4.0 release notes |
| 141 | + |
| 142 | +Uncommitted (pending v0.5.0): |
| 143 | +- Stdin-first context/quota, effort/fast display |
| 144 | +- Epoch timestamp support in reset formatters |
| 145 | +- 91 tests (was 85) |
| 146 | +- =.gitignore= for raw stdin dumps and cache files |
| 147 | + |
| 148 | +Real stdin captured from CLI v2.1.141: |
| 149 | +- =raw-stdin-max.json= (gitignored — contains session IDs and paths) |
| 150 | +- =raw-stdin-pro.json= (gitignored — same) |
0 commit comments