From bc0260b28b368f0b6a99d9a75640a0ed8be34a17 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Sun, 23 Aug 2026 23:38:41 -0700 Subject: [PATCH] feat(rows): the strip is a record, not a forecast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 5h strip stops at ▮. It drew the rest of the window as hollow cells and then, when linear pace said so, as a wall of × — `5h ▮▯××`, which a user could not read and was right not to. Three surfaces were saying one thing badly: the badge already prints when the window ends (`5h[38%@23:00]`), the notice already names the wall with an exact time and its own gates (`5h caps ~14:20`), and the strip dramatised both a third time in four columns. A 5h window is short enough that its future is a clock, not a shape. five_dry_cell is deleted rather than guarded — a guard on a projection nobody should read is still that projection. ▯ and × keep their meaning on the 7d strip, where the future is 34 cells long and has a shape, and in the `week` report. The 7d future folds as soon as folding hides two cells. The old threshold of 10 was justified by a column break-even, which measured the wrong thing: eleven hollow cells read as too much future long before they got expensive. The tail is now at most ▯▯▯ raw, or ▯▯...▯(✕N) — and N, the windows left, is the part that must survive mid-week, when a pressure notice owns the pin and this row is the only place it appears. A closing week draws to its edge with no special case. Supersedes the five_dry_cell evidence floor from v0.32.1; the row-3 floor from that release stands unchanged. 427 tests (was 426). Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 29 +++++++++++++++++ DESIGN.md | 23 +++++++------ README.md | 39 +++++++++++++--------- docs/index.html | 12 +++---- llms.txt | 8 ++--- statusline.sh | 67 +++++++++++++++++++------------------- t/helpers.bash | 4 +-- t/statusline.bats | 82 ++++++++++++++++++++++++++++++++++++----------- 8 files changed, 175 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea73cdc..b2252c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,35 @@ ## Unreleased +## v0.33.0 — 2026-08-23 — the strip is a record, not a forecast + +**The 5h strip stops at `▮`.** It used to draw the rest of the window as +hollow cells and then, when linear pace said so, as a wall of `×` — +`5h ▮▯××`, which a user could not read, and was right not to. Three +things were saying one thing badly: the badge above already prints when +this window ends (`5h[38%@23:00]`), the notice engine already names the +wall with an exact time and its own gates (`5h caps ~14:20`), and the +strip was dramatising both a third time in four columns. A 5h window is +short enough that its future is a clock, not a shape. The strip now +draws what happened and ends — no `▯`, no `×`, no projection at all, and +`five_dry_cell` is gone rather than guarded. Strips carry history, badges +carry state, notices do the warning. + +Both glyphs keep their meaning on the 7d strip, where the future is 34 +cells long and genuinely has a shape, and in the `week` report. + +**The 7d future folds as soon as folding hides two cells.** The old +threshold was 10, justified by a column break-even that measured the +wrong thing: eleven hollow cells read as "too much future" long before +they got expensive. With two kept cells the tail is now at most `▯▯▯` +drawn raw, or `▯▯...▯(✕N)` — and `N`, the windows you have left, is the +part that has to survive mid-week, when a pressure notice owns the pin +and this row is the only place that number appears. A closing week still +draws to its edge with no special case: four cells or fewer leave +nothing worth folding. + +Row 3's floor (`notice_flash_worth_row`) shipped in v0.32.1, below. + ## v0.32.1 — 2026-08-23 — one projection, one floor **Row 3's gate was measuring the wrong thing.** It required the flash to diff --git a/DESIGN.md b/DESIGN.md index 8bf7ed8..7368362 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -82,16 +82,22 @@ own signal. ## The ledgers (row 2) ``` -5h ▃▄▮▯▯ 0.6x @04:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▮▯▯...▯(x14) 0.7x @Wed 09:00 +5h ▃▄▮ 0.6x @04:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▮▯▯...▯(x14) 0.7x @Wed 09:00 ``` -One grammar, two scales. `5h` = this window as 5 hours; `7d` = the -period as 34 five-hour slots, oldest left, a gap at each local midnight -*in history only* — the run from `▮` on is contiguous. History draws in +One grammar, two scales. `5h` = the hours of this window that have +happened; `7d` = the period as 34 five-hour slots, oldest left, a gap at +each local midnight *in history only* — the run from `▮` on is +contiguous. Only the 7d strip draws a future at all: the 5h one stops at +`▮`, because when this window ends is the badge's job (`5h[38%@23:00]`) +and whether it caps is the notice's (`5h caps ~14:20`). Strips carry +history, badges carry state, notices do the warning. History draws in full; the live row folds the 7d future after two kept cells into `...▯(x14)` — the 5h windows left before the reset, all alike, `×` red when the tail projects dry (the `week` report still draws every slot). The -count is the budget line's own, priced from the same instant. Each strip ends +count is the budget line's own, priced from the same instant, and it folds +as soon as folding hides two cells: the future is one fact, and the ink +belongs to history. Each strip ends with its pace (used ÷ elapsed; dim <1x, pressure ≥1x, hidden until 5% of the window has run) and the reset its right edge is — axis labels, not restated badges. @@ -100,11 +106,10 @@ badges. ▁▂▃▄▅▆▇█ burned; height = points that cell cost (▁ ≤2 … ▅ ≤11 … █ >20) ˍ ran, negligible — a bar of height zero, on the baseline ░ unknown — no sample; never drawn as idle -▮ now -▯ ahead — the hollow of ▮ +▮ now — and the last cell of the 5h strip +▯ ahead — the hollow of ▮ (7d only) ...▯(x14) the folded future: 14 more 5h windows before the reset (live 7d row) -× pace won't cover it (7d: learned forecast, linear when cold; 5h: linear; - no projection before 5% of the window has run — same floor as the pace) +× pace won't cover it (7d only: learned forecast, linear when cold) ``` Burn cells take their badge's pressure color; nothing else in the row is diff --git a/README.md b/README.md index 60669cf..8a1d4c4 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Every component earns its place: | User tier | Neutral white-weight (MAX bold, PRO normal, dim otherwise) — identity, never a status color. Truncated display name. | | Quota | Integer percentages. The 5h badge always carries its reset time while a window is live — `5h[42%@14:30]` reads "42% used, resets at 14:30" — because on a 5h horizon the reset is the number you plan the current sitting around. Wall-clock, not a countdown, on purpose: Claude Code only re-renders the statusline on activity, so a relative "@1h38m" silently decays into a lie during idle gaps, while "@14:30" stays true in a frozen frame. (The 7d badge is hybrid: day-relative `@5d` while the reset is >= 24h out — decays one day per day, mild and narrow — switching to the same wall-clock `@04:00` inside the last day, where an `@6h`/`@<1h` countdown decayed by the hour exactly when pressure keeps the suffix visible.) When a window's utilization climbs between renders, a reverse-video `+N` token appears right after the badge for ~60s: `5h[44%@14:32]+2` means "you just burned 2%". A drop (window reset) stays quiet — the fresh low number is its own signal. **7d is forecast, not leveled**: a learned per-weekday burn profile (EWMA over your own usage history) plus your recent 24h burn project whether the quota outlasts the window — your heavy Tuesday counts more than a generic average. The verdict is color alone; under pressure the badge shows when relief arrives: `7d[44%@5d]` red means "at your pace, dry days before the reset 5 days from now"; inside the last day it reads `7d[92%@04:00]` — resets at 04:00. Cold start (<14 days history) falls back to window-average pacing. Recovery color when reset is imminent. **Model-scoped weekly quota**: when the usage API carries a per-model weekly limit (`limits[]`, `kind=weekly_scoped`) for the model your session is running, it renders right after the model+context block — `fabl5[1m][12%] fb[67%]` on a Fable 5 session, `op[33%]` on Opus — because the quota is a property of the model you're running, not of the account-wide 5h/7d cluster. It's a weekly number (same reset as the 7d badge), scoped to one model. Other models' scoped quotas stay hidden: only the limit constraining *this* session is signal. Supersedes the legacy `seven_day_opus`/`seven_day_sonnet` fields, which the API now sends as null. | | Extra usage | Monthly spend, limit, prepaid balance. `--extra auto` shows when quota runs out. | -| Week row | **A row of its own, under the badges**: `5h ▅█▃▮▯ 0.9✕ @23:00 7d ▅▁▂ ▃▅ˍ▃▅ …▮▯▯...▯(✕19) 0.7✕ @Wed 09:00` — this sitting by the hour, the week as its 5h windows (day-gapped, the far future folded to a counted `...▯(✕19)`), height = what each cell burned, `▮` now, `×` where the pool runs dry, each strip ending with its pace and reset. Reconstructed from your own usage log; `auto` shows it once there is history to show. See [Week row](#week-row). | +| Week row | **A row of its own, under the badges**: `5h ▅█▃▮ 0.9✕ @23:00 7d ▅▁▂ ▃▅ˍ▃▅ …▮▯▯...▯(✕19) 0.7✕ @Wed 09:00` — this sitting by the hour, the week as its 5h windows (day-gapped, the far future folded to a counted `...▯(✕19)`), height = what each cell burned, `▮` now and, on the 5h strip, the end of it — that strip is history, the badge above already says when the window closes. `×` marks where the pool runs dry on the week; each strip ends with its pace and reset. Reconstructed from your own usage log; `auto` shows it once there is history to show. See [Week row](#week-row). | | Deadman | **Invisible until a switch is armed.** Surfaces [deadman](https://github.com/thevibeworks/deadman) — a dead man's switch that hands the session off when you stop responding. `[☠ armed 42m]` (dim) counts down to the auto-handoff; `[☠ warned 3m]` (yellow) means the phone warning went out; `[☠ due]` means the handoff fires imminently. Sits on the left lane next to the path — it describes this session's lifecycle, not a quota. One `command -v` when the tool is absent, one fast file read when present; nothing armed renders nothing. `--deadman off` disables it. | | Cache health | **Quiet until it bites.** Claude Code never re-renders an idle session, and while you work the prompt cache is always freshly ~1 TTL from expiry — so a proactive "expiring soon" isn't honestly observable, and `auto` spends no width on it. It speaks only when a rewrite actually happens: `≡!419k` the instant you resume onto a dead cache (idle longer than the TTL) or a mid-session prefix collapse — a 419k-token re-cache at ~20x the read rate (and the same burn on your 5h/7d quota on subscriptions). **Bold red past 200k** — the premium-band miss. `≡~` while a large prefix rebuilds. `--cache always` additionally keeps the freeze-safe deadline `≡@15:20` (last request + TTL; a past time in a frozen frame reads "expired at 15:20"). TTL defaults to 1h (claude.ai subscriber sessions) or 5m (API-key auth); an observed usage breakdown overrides it. The `≡` glyph (U+2261) reads as stacked cache layers — one terminal column, quiet and distinct. | @@ -255,7 +255,7 @@ right edge shared with line 1: ```text proj (main*) +84/-14 8m $6.72 fabl5[1m][██░░42%] fb[66%] [MAX|@work] 5h[38%@23:00] 7d[39%] -3✕5h left · 20%/win 5h ▅█▃▮▯ 0.9✕ 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7✕ @Wed 09:00 +3✕5h left · 20%/win 5h ▅█▃▮ 0.9✕ 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7✕ @Wed 09:00 ``` The 5h strip prints no reset: line 1's `5h[38%@23:00]` already carries @@ -272,7 +272,7 @@ edge and the full sentence sits flush-left beneath them: ```text proj (main*) fabl5[1m][██░░42%] fb[66%] [MAX|@work] 5h[38%@23:00] 7d[39%] - 5h ▅█▃▮▯ 0.9✕ @23:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7✕ @Wed 09:00 + 5h ▅█▃▮ 0.9✕ @23:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7✕ @Wed 09:00 budget ~3✕5h left · even 20%/win · heading ~52% ``` @@ -285,18 +285,25 @@ budget line shows with it (windows left, what even looks like, where you land); pressure and surplus clauses still take its place when they fire. -- **`5h`** — this sitting: the current 5h window as 5 hour cells, - height = the 5h points that hour added (each positive step between - consecutive samples credited to the hour the later sample fell in). +- **`5h`** — this sitting: the hours of the current 5h window that have + happened, height = the 5h points that hour added (each positive step + between consecutive samples credited to the hour the later sample fell + in). It ends at `▮` and draws no future: when the window closes is what + the badge says (`5h[38%@23:00]`), and whether you will hit the wall is + what the notice says (`5h caps ~14:20`) — three hollow cells and a row + of `×` only said it a third time, louder. - **`7d`** — the week: the 7d period as its 5h windows (34 slots, oldest left, the last a 3h stub), height = the 7d points that window burned, with a thin gap at each local midnight so days read as clusters — and a day that held five windows shows it — without a ruler. History draws in - full; the future folds: two hollow cells after `▮`, then `...▯(✕28)` — - 28 more 5h windows before the reset, all alike (`×` red when the tail - projects dry). That count is the budget line's own `~28✕5h left`, priced - from the same instant: one row, one arithmetic. The `week` subcommand's - wide ledger still draws every slot. + full; the future folds as soon as folding hides two cells: two hollow + cells after `▮`, then `...▯(✕28)` — 28 more 5h windows before the reset, + all alike (`×` red when the tail projects dry). That count is the budget + line's own `~28✕5h left`, priced from the same instant: one row, one + arithmetic. The future is one fact and the strip's ink belongs to + history; a closing week needs no help to draw to its edge, since four + cells or fewer leave nothing worth folding. The `week` subcommand's wide + ledger still draws every slot. | Cell | Meaning | |------|---------| @@ -304,8 +311,8 @@ fire. | `ˍ` | ran, cost under a point — or ran idle inside the log's coverage; a bar of height zero, on the baseline | | `░` | unknown: the log has no sample for that cell (never drawn as idle — a gap in the record is not a quiet session) | | `▮` | the cell you are in now | -| `▯` | a cell still ahead of you — the hollow of `▮`, an empty slot waiting | -| `×` | a cell the pool will not cover at the current pace (7d: the learned forecast's dry point, linear when untrained; 5h: linear, the same projection as the badge). Waits on the same evidence the pace suffix does — 5% of the window — so one front-loaded prompt cannot wall off a window that just opened | +| `▯` | a cell still ahead of you — the hollow of `▮`, an empty slot waiting. **7d only**: the 5h strip stops at `▮` | +| `×` | a cell the pool will not cover at the current pace — **7d only**: the learned forecast's dry point, linear when untrained. The 5h window's wall belongs to the `5h caps ~14:20` notice, which states the time instead of shading cells | | `...▯(✕28)` | the folded 7d future: 28 more 5h windows before the reset, one token instead of a run of hollow cells (`×` red when the tail ends dry). The count is windows-to-reset — what the budget line prices — not the number of cells the fold happened to hide | | `✕` | not a cell — the multiplication sign, the row's one operator (`...▯(✕28)`, `0.7✕`, `19✕5h left`). Deliberately not `×` (U+00D7), which is already a reading: cells are the ink, the operator is punctuation, and `...×(×28)` has to say both at once. One terminal column and no emoji fallback, so the row still meets line 1's edge; override with `MULT_GLYPH` (`╳` and `✖` look stronger but are ambiguous-width and emoji-presentation respectively) | @@ -318,7 +325,7 @@ projects the rest. Freeze-safe by construction: `▮` moves at cell boundaries and every other cell is history. `--week auto` (default) draws the row only once the log holds a sample -for either period — a fresh install gets no `░░░▮▯▯` row that says nothing +for either period — a fresh install gets no `░░░▮` row that says nothing the badges don't. `--week always` draws it whenever a window is live; `--week off` never. The 7d strip is the same one `statusline.sh week` prints; both are cached in `week.cache` and rebuilt only when the log @@ -334,7 +341,7 @@ long one: ```text proj (main*) +84/-14 8m $6.72 fabl5[1m][██░░42%] fb[91%] [MAX|@work] 5h[38%@23:00] 7d[55%] -+ fb 91% vs 7d 55% · go op 5h ▅█▃▮▯ 0.9✕ 7d ▅▁▂ ▃▅ˍ▃▅ … ▆▆ˍ▂▮▯▯ 0.6✕ @Wed 09:00 ++ fb 91% vs 7d 55% · go op 5h ▅█▃▮ 0.9✕ 7d ▅▁▂ ▃▅ˍ▃▅ … ▆▆ˍ▂▮▯▯ 0.6✕ @Wed 09:00 + fb weekly 91% against 7d 55% · the model caps first, not the account; op sits at 33%, so run it for the bulk ``` @@ -513,7 +520,7 @@ run. Setting only `CLAUDE_CACHE_DIR` keeps the legacy single-dir behavior. npm exec --yes bats -- t/ ``` -426 tests across `t/statusline.bats` (414 statusline + integration) and +427 tests across `t/statusline.bats` (415 statusline + integration) and `t/install.bats` (12 installer). CI runs on push and PR to `main`. ## Project Structure diff --git a/docs/index.html b/docs/index.html index 05c6dd3..379f874 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ Claude Code Statusline — a meter for your Claude Code plan - + @@ -204,7 +204,7 @@

claude-code-statusline — a meter for your Claude Code plan

-
426 tests · jq + curl · no daemon · no telemetry
+
427 tests · jq + curl · no daemon · no telemetry
@@ -247,8 +247,8 @@

Read the meter

The 7-day window, forecast-colored. A learned per-weekday burn profile projects whether the pool outlasts the window — your heavy Tuesday counts more than a generic average. Red answers: at your pace, dry before the reset.
-
5h ▂▅█ 0.8✕№ 0001
-
The 5h ledger — this sitting by the hour. Height is what each hour cost; now, ahead, ˍ ran-but-negligible, no sample. Ends with its pace: above 1✕ you cap before the reset.
+
5h ▂▅█ 0.8✕№ 0001
+
The 5h ledger — the hours of this sitting that have happened. Height is what each hour cost; now and the end of the strip, ˍ ran-but-negligible, no sample. It draws no future: when the window closes is the badge's job, whether it caps is the notice's. Ends with its pace: above 1✕ you cap before the reset.
7d ▃▅ˍ▃▅ ▃▃▁▂▁▯▯...▯(✕14) 0.7✕№ 0001
@@ -277,7 +277,7 @@

Built like a meter should be

Numbers that don't lie

Wall-clock resets instead of decaying countdowns. A visible [░░░░░░0%] after /compact instead of a hidden bar. Stale data is badged, never passed off as fresh. Burn is a monotone envelope, never a re-earned dip.

Nothing running in the background

-

No daemon, no telemetry, no npm. It runs when Claude Code renders the prompt, reads stdin plus one shared cached API call, prints its rows, exits. 426 bats tests on the one file.

+

No daemon, no telemetry, no npm. It runs when Claude Code renders the prompt, reads stdin plus one shared cached API call, prints its rows, exits. 427 bats tests on the one file.

@@ -327,7 +327,7 @@

What it is not

// Rows beneath line 1 hang as one block: its right edge meets line 1's // edge, every row shares one left edge — exactly how the script pads them. // 5h ledger: 5 hour cells. 7d ledger: history in full, future folded. - var L5 = s('d','5h ') + s('g','▂▅█') + s('w','▮') + s('d2','▯') + s('d',' 0.8✕'); + var L5 = s('d','5h ') + s('g','▂▅█') + s('w','▮') + s('d',' 0.8✕'); var L5b = s('d','5h ') + s('g','▂▅█▄') + s('w','▮') + s('d',' 0.9✕'); var L7 = s('d','7d ') + s('y','▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ') + s('w','▮') + s('d2','▯▯...▯(✕14)') + s('d',' 0.7✕ @Wed 09:00'); var L7x = s('d','7d ') + s('y','▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ') + s('w','▮') + s('d2','▯▯') + s('r','...×(✕14)') + s('d',' 0.9✕ @Wed 09:00'); diff --git a/llms.txt b/llms.txt index dd2bf7c..a940da5 100644 --- a/llms.txt +++ b/llms.txt @@ -3,12 +3,12 @@ > One bash file that renders live quota (5h/7d windows with reset times, > per-model weekly limits), context pressure, session cost, prompt-cache > health, and git activity in Claude Code's statusLine hook — plus a -> week row (this 5h window by the hour, the 7d period as its 5h -> windows with the future folded to a counted `...▯(✕28)` tail: where -> the points went), an advisor row that interprets the +> week row (the hours of this 5h window that have happened, the 7d +> period as its 5h windows with the future folded to a counted +> `...▯(✕28)` tail: where the points went), an advisor row that interprets the > numbers (cap projections, expiring-surplus and underuse advice, fleet > relief), and a `report` subcommand that ledgers what each closed window -> expired unused. 426 bats tests, deps are jq + curl, no daemon, no +> expired unused. 427 bats tests, deps are jq + curl, no daemon, no > telemetry. Key facts for agents: diff --git a/statusline.sh b/statusline.sh index 4f35bf4..977fa5b 100755 --- a/statusline.sh +++ b/statusline.sh @@ -2670,9 +2670,10 @@ run_session_summary() { # ˍ ran, burned under a point, or idle inside the log's coverage — a # bar of height zero, on the baseline # ░ unknown — outside the sample log's coverage -# ▮ the cell you are in now -# ▯ a cell still ahead of you (the hollow of ▮: an empty slot) -# × a cell the pool will not cover at the current pace +# ▮ the cell you are in now — and the END of the 5h strip: that +# strip is history, it has no cells after this one +# ▯ a cell still ahead of you (the hollow of ▮: an empty slot) — 7d +# × a cell the pool will not cover at the current pace — 7d # ...▯(✕12) the folded future: 12 more 5h windows before the reset, all # alike (× red when the tail projects dry) — live 7d strip only. # The count is windows-to-reset, the same number the budget line @@ -2683,6 +2684,11 @@ run_session_summary() { # ✕ is the operator, × is a cell: the two never mean the same. # Unknown and idle are deliberately different glyphs: drawing a gap in the # record as an idle session is the one lie this row must not tell. +# Only the 7d strip draws a future. The 5h one stops at ▮: a window whose end +# the badge already states (`5h[38%@23:00]`) and whose wall a notice already +# names (`5h caps ~14:20`) does not need three hollow cells and a row of × +# to say it a third time, louder. Strips carry history, badges carry state, +# notices do the warning. # Shared by the live `--week` row and the `week` subcommand, so the two # surfaces cannot disagree. @@ -2693,10 +2699,15 @@ WEEK_CACHE_TTL_SECS=300 # The live row compresses the 7d strip's future run: after the now-marker it # keeps WEEK_FUTURE_KEEP hollow cells, then folds the rest into `...▯(✕12)` # (count = 5h windows before the reset; × red when the tail projects dry). -# History is information, the future is all the same cell — but only fold -# when it hides enough to matter, so a closing week still draws to its edge. +# History is information; the future is one fact, and the fact is the count. +# The threshold is 2, not a column break-even: eleven hollow cells were +# measured to read as "too much future" long before they were expensive, and +# the count is the part that has to survive mid-week, when a pressure notice +# owns the pin and this row is the only place the remaining windows appear. +# A closing week still draws to its edge without help — four cells or fewer +# leave nothing worth folding. WEEK_FUTURE_KEEP=2 -WEEK_FUTURE_MIN_HIDE=10 +WEEK_FUTURE_MIN_HIDE=2 # The period start on the same 5-min grid the window keys are rounded to. # resets_at is jittered by the API (15:59:59.76 one fetch, 16:00:00.47 the @@ -2826,8 +2837,9 @@ week_dry_slot() { # The colored strip. Args: fill percent (for the pressure tint), now, # period start, dry cell (-1 none), history line ("lo hi slot:cost,..."), -# cell count, cell seconds, day-gaps flag, future cells kept before folding -# (0 = draw all), and the TRUE period length in seconds — the fold token +# cell count, cell seconds, day-gaps flag, how much future to draw (-1 none: +# the strip ends at ▮; 0 all; N keep N hollow cells then fold), and the TRUE +# period length in seconds — the fold token # counts windows to the real reset, not cells on the grid, so it needs the # period, not the drawing. Defaults to the grid when a caller has nothing # truer to offer. Cells and gaps come out of one awk so the row is one string @@ -2860,7 +2872,9 @@ build_ledger_strip() { # same hollow slot, so name it once with a count instead of # drawing it N times lim = w - if (fut > 0 && w - (nowslot + 1 + fut) >= minhide) + if (fut < 0) # history only: stop at the now-cell + lim = nowslot + 1 + else if (fut > 0 && w - (nowslot + 1 + fut) >= minhide) lim = nowslot + 1 + fut s = ""; prev = ""; pday = -1 for (i = 0; i < lim; i++) { @@ -2886,7 +2900,7 @@ build_ledger_strip() { if (c != prev) { s = s C_OFF c; prev = c } s = s g } - if (lim < w) { + if (fut >= 0 && lim < w) { # the fold: glyph = how the tail ends (× red when the pool # dries before the reset), count = 5h windows still to come # before the reset — the same number the budget line prices, @@ -2912,10 +2926,15 @@ build_week_strip() { build_ledger_strip "$1" "$2" "$3" "$4" "$5" "$WEEK_CELLS" 18000 1 "${6:-0}" "$SEVEN_DAY_WINDOW_SECS" } -# 5h strip: 5 ✕ 1h cells of the current window. $5 is -# five_history_cells' line. +# 5h strip: the hours of the current window that have HAPPENED, ending at ▮. +# $4 is five_history_cells' line. No hollow cells, no dry projection, no +# future of any kind: a strip carries history, the badge carries state +# (`5h[38%@23:00]` is the clock this window ends on) and a notice does the +# warning (`5h caps ~14:20`, with its own gates and its own exact time). +# Three empty cells and a wall of × said none of that — they spent four +# columns dramatising a countdown the badge above already prints. build_five_strip() { - build_ledger_strip "$1" "$2" "$3" "$4" "$5" "$FIVE_CELLS" "$FIVE_CELL_SECS" 0 + build_ledger_strip "$1" "$2" "$3" -1 "$4" "$FIVE_CELLS" "$FIVE_CELL_SECS" 0 -1 } # Does a history line carry at least one cell BEFORE the now-cell? A single @@ -2948,24 +2967,6 @@ window_evidence_floor() { echo $(( $1 / 20 )) } -# The 5h window's own dry cell: linear pace, the same projection the 5h -# badge and the advisor's "5h caps ~14:20" use — so it waits for the same -# evidence they do. One prompt front-loading 7% ten minutes in is not a rate -# yet, and a wall of × is the loudest way to say it. -1 when it holds to -# reset, and while the window is too young to judge. -five_dry_cell() { - local five_int="$1" five_secs="$2" now="$3" five_start="$4" - local elapsed=$(( now - five_start )) floor - floor=$(window_evidence_floor 18000) - [ "$five_int" -gt 0 ] 2>/dev/null && [ "$elapsed" -ge "$floor" ] || { echo -1; return 0; } - local dry_epoch=$(( now + elapsed * (100 - five_int) / five_int )) - if [ "$dry_epoch" -lt $(( now + five_secs )) ]; then - echo $(( (dry_epoch - five_start) / FIVE_CELL_SECS )) - else - echo -1 - fi -} - # The current 5h window's start on the 5-min grid (its resets_at - 5h). five_period_start() { local now="$1" five_secs="$2" @@ -3053,9 +3054,7 @@ build_week_row() { fi local parts="" if [ "$have_five" = 1 ]; then - local fdry - fdry=$(five_dry_cell "$five_int" "$five_secs" "$now" "$five_start") - parts="${DIM}5h ${RESET}$(build_five_strip "$five_int" "$now" "$five_start" "$fdry" "$five_hist")$(strip_tail "$five_int" "$five_secs" 18000 "$now" "$five_reset_mode")" + parts="${DIM}5h ${RESET}$(build_five_strip "$five_int" "$now" "$five_start" "$five_hist")$(strip_tail "$five_int" "$five_secs" 18000 "$now" "$five_reset_mode")" fi if [ "$have_seven" = 1 ]; then local dry diff --git a/t/helpers.bash b/t/helpers.bash index d0d442c..2427f76 100644 --- a/t/helpers.bash +++ b/t/helpers.bash @@ -76,7 +76,7 @@ STDIN_RL_FETCH_TTL=120 FIVE_CELLS=5 FIVE_CELL_SECS=3600 WEEK_FUTURE_KEEP=2 -WEEK_FUTURE_MIN_HIDE=10 +WEEK_FUTURE_MIN_HIDE=2 WEEK_CACHE_TTL_SECS=300 NOTICE_FLASH_SECS=90 NOTICE_FLASH_MIN_CHARS=16 @@ -96,7 +96,7 @@ debug_log() { # Source individual functions by extracting them from statusline.sh. # This is deliberate: we test the actual production code, not copies. eval "$(awk ' - /^(abbreviate_model_id|get_runtime_model|format_reset_relative|format_reset_absolute|get_reset_seconds|format_duration|should_show_extra|get_cache_health|infer_cache_ttl_class|build_cache_indicator|get_usage_color|get_seven_day_color|seven_day_elapsed|seven_day_pace|weekend_secs_ahead|get_adaptive_ttl|curl_ca_bundle|acquire_lock|reap_stale_lock|fetch_usage_for_session|merge_stdin_rate_limits|rotate_usage_log|build_seven_day_profile|seven_day_forecast|premium_band_level|abbrev_effort|effort_color|_epoch_from_ts|_fmt_epoch|render_bar|format_money_minor|oauth_token_expired|refresh_oauth_credentials_file|is_default_1m_family|get_context_limit|is_1m_model|rotate_debug_log|build_display_path|build_trace_url|build_trace_component|delta_flash|delta_flash_part|quota_bump_notice|record_fetch_error|fetch_error_remaining|fetch_error_badge|model_scope_abbrev|build_scoped_quota_display|build_usage_display|build_extra_usage_display|build_user_info|get_user_tier|build_advisor_line|build_advisor_fleet_hint|_seven_day_walk|forecast_pct_per_window|build_deadman_component|log_usage_snapshot|log_stdin_snapshot|detect_session_boundary|last_logged_model|run_usage_report|run_check|run_session_summary|run_week|week_period_start|week_scan|week_history_cells|five_history_cells|week_dry_slot|ledger_has_past|window_evidence_floor|build_ledger_strip|build_week_strip|build_five_strip|five_dry_cell|five_period_start|strip_tail|build_week_row|compact_text|plain_text|notice_add|notice_voice_color|notice_highlight|notice_render|notice_first_seen|notice_ranked|notice_pin_line|notice_long_line|notice_flash_line|notice_flash_worth_row|notice_collect|_profile_walk|_scoped_walk|scoped_forecast|scoped_profile_name|forecast_usd_per_pct|session_telemetry_json)\(\)/ { capture=1 } + /^(abbreviate_model_id|get_runtime_model|format_reset_relative|format_reset_absolute|get_reset_seconds|format_duration|should_show_extra|get_cache_health|infer_cache_ttl_class|build_cache_indicator|get_usage_color|get_seven_day_color|seven_day_elapsed|seven_day_pace|weekend_secs_ahead|get_adaptive_ttl|curl_ca_bundle|acquire_lock|reap_stale_lock|fetch_usage_for_session|merge_stdin_rate_limits|rotate_usage_log|build_seven_day_profile|seven_day_forecast|premium_band_level|abbrev_effort|effort_color|_epoch_from_ts|_fmt_epoch|render_bar|format_money_minor|oauth_token_expired|refresh_oauth_credentials_file|is_default_1m_family|get_context_limit|is_1m_model|rotate_debug_log|build_display_path|build_trace_url|build_trace_component|delta_flash|delta_flash_part|quota_bump_notice|record_fetch_error|fetch_error_remaining|fetch_error_badge|model_scope_abbrev|build_scoped_quota_display|build_usage_display|build_extra_usage_display|build_user_info|get_user_tier|build_advisor_line|build_advisor_fleet_hint|_seven_day_walk|forecast_pct_per_window|build_deadman_component|log_usage_snapshot|log_stdin_snapshot|detect_session_boundary|last_logged_model|run_usage_report|run_check|run_session_summary|run_week|week_period_start|week_scan|week_history_cells|five_history_cells|week_dry_slot|ledger_has_past|window_evidence_floor|build_ledger_strip|build_week_strip|build_five_strip|five_period_start|strip_tail|build_week_row|compact_text|plain_text|notice_add|notice_voice_color|notice_highlight|notice_render|notice_first_seen|notice_ranked|notice_pin_line|notice_long_line|notice_flash_line|notice_flash_worth_row|notice_collect|_profile_walk|_scoped_walk|scoped_forecast|scoped_profile_name|forecast_usd_per_pct|session_telemetry_json)\(\)/ { capture=1 } capture { print } capture && /^}$/ { capture=0 } ' "$SCRIPT_DIR/statusline.sh")" diff --git a/t/statusline.bats b/t/statusline.bats index 296b43d..b75f28d 100644 --- a/t/statusline.bats +++ b/t/statusline.bats @@ -1198,7 +1198,7 @@ _seed_week_store() { done } -@test "build_week_row: the strip under the badges, labelled 7d, 34 cells" { +@test "build_week_row: the strip under the badges, labelled 7d, on a 34-cell grid" { tmpdir=$(mktemp -d) CLAUDE_ACCOUNT_DIR="$tmpdir" _seed_week_store "$tmpdir" @@ -1208,7 +1208,15 @@ _seed_week_store() { [[ "$plain" == "7d "* ]] strip=$(printf '%s' "${plain#7d }" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//') bar=$(printf '%s' "$strip" | tr -d ' ') - [ "$(echo "$bar" | grep -o . | wc -l)" -eq 34 ] + # the live row draws history + ▮ + WEEK_FUTURE_KEEP and folds the rest; + # the grid it draws them on is still 34 cells (`week` renders all of it) + [[ "$bar" =~ ^(.*)\.\.\.▯\(✕[0-9]+\)$ ]] + drawn="${BASH_REMATCH[1]}" + seven_secs=$(get_reset_seconds "$(jq -r .seven_day.resets_at "$tmpdir/usage.cache")") + ps=$(week_period_start "$(date +%s)" "$seven_secs") + nowslot=$(( ($(date +%s) - ps) / 18000 )) + [ "$(printf '%s' "$drawn" | grep -o . | wc -l)" -eq $((nowslot + 1 + WEEK_FUTURE_KEEP)) ] + [ $((nowslot + 1 + WEEK_FUTURE_KEEP)) -lt 34 ] [[ "$bar" =~ [▁▂▃▄▅▆▇█]ˍ[▁▂▃▄▅▆▇█] ]] [[ "$bar" == *▮* ]] # day gaps: single spaces inside the strip, several of them across a week @@ -1259,24 +1267,36 @@ _seed_week_store() { [[ "$plain" =~ @[A-Z][a-z][a-z]\ [0-9]{2}:[0-9]{2}$ ]] } -@test "five_dry_cell: a fresh 5h window projects nothing — one floor for the trio" { - now=$(date +%s) - # Live report: one big prompt front-loaded 7% ten minutes into a window - # and the strip drew `▮▯×××` — while the pace suffix beside it judged - # itself too young to speak and the "5h caps" notice stayed silent. Same - # projection, three surfaces; they wait on the same evidence now. - [ "$(five_dry_cell 7 $((18000 - 600)) "$now" $((now - 600)))" = "-1" ] - [ "$(window_evidence_floor 18000)" = "900" ] - # past the floor the same burn is a rate, and the wall comes back - [ "$(five_dry_cell 7 $((18000 - 900)) "$now" $((now - 900)))" != "-1" ] - # the strip is what the user actually sees +@test "build_five_strip: the 5h strip is history — it ends at ▮, always" { + # The user could not read `5h ▮▯××`, and they were right: the future of a + # 5h window is a clock the badge already prints (`5h[7%@04:20]`) and the + # dry projection duplicates the rank-90 "5h caps" notice, which owns that + # warning with its own gates and its own exact time. tmpdir=$(mktemp -d) CLAUDE_ACCOUNT_DIR="$tmpdir" - reset_5h=$(date -u -d "@$((now + 18000 - 600))" '+%Y-%m-%dT%H:%M:%SZ') - usage=$(printf '{"fetched_at":%s,"five_hour":{"utilization":7,"resets_at":"%s"},"seven_day":{"utilization":20}}' "$now" "$reset_5h") - five=$(strip_ansi "$(build_week_row "$usage" always)") - [[ "$five" == "5h "* ]] - [[ "$five" != *×* ]] + now=$(date +%s) + # (1) a hot young window: one prompt front-loaded 7% ten minutes in — + # the fixture that used to draw ▮▯××× + _five_of() { + local reset_5h; reset_5h=$(date -u -d "@$1" '+%Y-%m-%dT%H:%M:%SZ') + local u=$(printf '{"fetched_at":%s,"five_hour":{"utilization":%s,"resets_at":"%s"},"seven_day":{"utilization":20}}' "$now" "$2" "$reset_5h") + local p; p=$(strip_ansi "$(build_week_row "$u" always)") + p="${p#5h }"; p="${p%% 7d*}" + printf '%s' "$(printf '%s' "$p" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//; s/ [0-9.]*✕$//')" + } + young=$(_five_of $((now + 18000 - 600)) 7) + [[ "$young" == *▮ ]] + [[ "$young" != *▯* ]] + [[ "$young" != *×* ]] + # (2) mid-window, burning hard enough that linear pace would have walled + # off every remaining cell + mid=$(_five_of $((now + 18000 - 9000)) 80) + [[ "$mid" == *▮ ]] + [[ "$mid" != *▯* ]] + [[ "$mid" != *×* ]] + # ▯ and × are not gone from the vocabulary — the 7d strip still draws both + plain=$(strip_ansi "$(build_week_row "$(printf '{"fetched_at":%s,"five_hour":{"utilization":7},"seven_day":{"utilization":20,"resets_at":"%s"}}' "$now" "$(date -u -d '+5 days' '+%Y-%m-%dT%H:%M:%SZ')")" always)") + [[ "$plain" == *▯* ]] rm -rf "$tmpdir" } @@ -1403,6 +1423,32 @@ _seed_week_store() { rm -rf "$tmpdir" } +@test "build_week_row: the future folds as soon as it hides two cells" { + # The threshold is not a column break-even. The future is one fact — the + # count — and mid-week, when a pressure notice owns the pin, this row is + # the only place the remaining windows appear. Eleven hollow cells read as + # "too much future" long before they were expensive. + tmpdir=$(mktemp -d) + CLAUDE_ACCOUNT_DIR="$tmpdir" + now=$(date +%s) + _tail_of() { + local reset; reset=$(date -u -d "@$1" '+%Y-%m-%dT%H:%M:%SZ') + local u; u=$(printf '{"fetched_at":%s,"five_hour":{"utilization":9},"seven_day":{"utilization":20,"resets_at":"%s"}}' "$now" "$reset") + local p; p=$(strip_ansi "$(build_week_row "$u" always)" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//' | tr -d ' ') + printf '%s' "${p#*▮}" + } + # ~6 windows left: KEEP=2 drawn, the rest folded and counted + six=$(_tail_of $((now + 6 * 18000 - 3600))) + [[ "$six" =~ ^▯▯\.\.\.▯\(✕([0-9]+)\)$ ]] + [ "${BASH_REMATCH[1]}" -eq 6 ] + # the last cells of the week: with one cell to hide the fold would spend + # more ink than it saves, so the strip draws to its edge + close=$(_tail_of $((now + 18000 + 600))) + [[ "$close" != *"..."* ]] + [[ "$close" =~ ^▯{0,3}$ ]] + rm -rf "$tmpdir" +} + @test "build_week_row: the fold token and the budget line count the same windows" { # one line, one arithmetic: the row folded `...▯(✕N)` and the budget # sentence beside it priced `~N✕5h left` from the same instant, and a