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