Repository navigation
feat(widgets): add editable label prefix to text-label widgets - #601
Conversation
Widgets that prepend a text label when raw value is off ("Model: ",
"Ctx: ", "In: ", ...) now accept a per-item override stored in
metadata.label. The override replaces every label variant of a widget
verbatim; an empty override drops the label.
Widgets opt in via Widget.getLabelPrefix(item). Rendering goes through
formatRawOrLabeledValue, which inline-label widgets now use too. The
items editor offers a shared la(b)el… keybind (hidden in raw value
mode) that opens a new LabelEditor, and lists overrides as
(label: "...").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extract CustomText's inline cursor handling into useTextCursor (src/widgets/shared/text-cursor.ts) and use it from both the Custom Text and label editors, giving the label editor cursor movement and forward delete. Ctrl+Left/Right now jump to the start/end as the editor hint says. The old checks compared input to 'ArrowLeft', which Ink never sends, and ran after the plain-arrow branch, so they were unreachable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es (#10) * feat(widgets): compact label presets with global and per-widget toggles Add abbreviated label presets for labeled widgets, complementary to the free-form label editing proposed upstream (sirmalloc#601): - New `compactLabels` settings flag (default off). Additive v4 key: old configs parse unchanged and render identically, no version bump needed. - Presets live in one map (`Model:` -> `M:`, `Context:` -> `Ctx:`, `Cost:` -> `` — the value's own `$` is the glyph) inside `formatRawOrLabeledValue`, so every widget routed through the helper picks them up with no per-widget changes. Model, Session Cost, and Context Bar moved onto the helper. - One-click global toggle in Global Overrides `(j)`; per-widget override via `(j) compact label` in the items editor (metadata `compactLabel`), which wins over the global setting in both directions. Capability for the editor keybind is derived from a cached preview probe of the widget registry. - Extend the Git Branch width-cap pattern to Current Working Dir and Session Name (`w` max-width keybind). - Document per-widget padding/separator/label width overhead in docs/USAGE.md; defaults unchanged. Tests: preset resolution, toggle metadata, global injection precedence, migration parity (v1->v4 and schema-less v4 parse), registry-wide `j` keybind reservation, capable-type probe sanity, Global Overrides `(j)` keystroke test. bun test 2400 pass; bun run lint clean. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(tui): compact-label toggle cycles over the effective state The per-widget (j) toggle only wrote/removed 'true', so with the global Compact Labels setting on it became a visual no-op: the render-time injection re-compactified the widget after the key was removed, and no row marker showed the effective state. - toggleCompactLabel now takes the settings and cycles the EFFECTIVE state: compact (saved 'true' or inherited global on) -> write 'false'; forced off -> drop the key and inherit; otherwise -> write 'true'. - getCompactLabelModifierText reports the effective state: '(compact label)' / '(compact label: on)' / '(compact label: off)'. - ItemsEditor/input-handlers pass settings through to both. - docs/USAGE.md: fix preset savings arithmetic (Model -4, Context -4). Co-Authored-By: Claude Code <noreply@anthropic.com> * test(renderer): cover (j) toggle forcing labels off while global compact is on Co-Authored-By: Claude Code <noreply@anthropic.com> * style: fix import order in compact-labels renderer test Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com>
eric-engberg
left a comment
There was a problem hiding this comment.
Thanks for this one. It's a feature I've wanted, and opting widgets in with getLabelPrefix keeps it tidy. I tried it and read through it, and ran into a few things:
-
Timer widgets in their bar modes. Block Timer, Block Reset Timer and Weekly Reset Timer render
Block(no colon) before a bar, butgetLabelPrefix()always returnsBlock:. In a bar mode the editor offersBlock:as the default. TypingBlock:there is then dropped bysetLabelas equal to the default, so the bar keepsBlock. The same goes forResetandWeekly Reset. Returning the bar-mode label fromgetLabelPrefix(item)in those modes, as Context % does for its two labels, would line them up. -
Changing a widget's type keeps the label. Changing the type with
→keeps the item's metadata, and nearly every labeled widget now readsmetadata.label. So a label set on Model (M) shows up on Session Cost after a type change (M $2.45). If the new type has no label, the old one stays hidden with nobto clear it, and comes back on the next type change. Droppinglabelwhen the type changes would avoid that. -
One source for the default label. Each widget states its label in
getLabelPrefix()and again as the literal it passes toformatRawOrLabeledValue, which is how the timer mismatch slipped in. If render took its label fromthis.getLabelPrefix(item), the two couldn't drift. -
useTextCursorand fast input.handleInputworks from thestatecaptured at render and callssetState(next). Two keys handled before a re-render apply to the same snapshot, and one is lost. A functional update,setState(prev => applyTextCursorInput(prev, input, key) ?? prev), avoids it. Custom Text's old code had the same pattern, so this isn't new, but the shared hook is a good place to fix it.
Smaller things:
LabelEditorputs the label and(default: …)in two siblingTexts. Ink lays out each code point of a toned or joined emoji (👍🏾, 👩💻) as its own character, so the secondTextoverwrites the end of such a label on screen; the saved value is fine. Nesting both in oneTextavoids it.getGraphemesintext-cursor.tsis private, so the sameIntl.Segmentercode is still inCustomSymbol.tsxandshared/symbol-override.tsx. Exporting it would leave one copy.renderTextWithCursorsplits the text twice and builds a new segmenter each call. One module-level segmenter and a single split would do.(label: "…")and the editor wrap the label in quotes without escaping it, so a label containing"reads ambiguously.JSON.stringify(label)would keep it clear.- After the cursor,
\x1b[0mresets every attribute;\x1b[27mwould end just the inverse.
sirmalloc#601 lets Tokens Input, Output, Cached and Total, and Git Staged, Unstaged and Untracked Files, offer their labels to the label editor one widget at a time. #16 and #22 move those widgets onto shared base classes, so the merges kept their shape and the labels went with it: an edited label still drew, but the editor never offered (e). Each base now offers its subclass's label, once for all of them.
…drift Block, Block Reset and Weekly Reset timers render a colon-less label in their bar modes, but getLabelPrefix always returned the colon form, so the label editor offered the wrong default and an override equal to the bar label was never saved. getLabelPrefix now returns the bar-mode label in those modes, and every widget's render takes its label from getLabelPrefix rather than repeating it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Changing a widget's type keeps its metadata, so a label written for one widget showed up on the next, or lingered hidden on a type without a label editor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useTextCursor applied each key to the state captured at render, so two keys handled before a re-render edited the same snapshot and one was lost. Edits now go through a functional state update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The label and its default sat in sibling Texts. Ink measures a toned or joined emoji as several columns, so the default overwrote the end of such a label on screen. Both now share one Text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Custom Symbol and the symbol override editor each kept their own copy of the Intl.Segmenter split that text-cursor already has. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
renderTextWithCursor split the text twice per render, and every split built a new Intl.Segmenter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
\x1b[0m reset every attribute, so any styling around the edited text stopped at the cursor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The (label: …) modifier and the editor's default wrapped the label in quotes without escaping it, so a label containing " read ambiguously. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setLabel dropped an override equal to the default for the widget's current mode, so a widget whose default changes with its mode could not pin that label for every mode (e.g. "Block " set in a bar mode still rendered "Block: " in time mode). Enter now always saves the typed label, and Tab clears the override instead of filling in the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loading placeholder takes the bar-mode label, so it reads "Reset [Loading]" rather than "Reset: [Loading]" in progress mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@eric-engberg thanks for the review! My tweaks worked at first pass for me and I admittedly didn't do much more thorough testing. I've addressed the feedback, PR description is updated accordingly. |
Upstream's label editor (sirmalloc#601) gives each widget one default label, while Extra Usage Used, Remaining and Utilization pick "Overage" or "Spend" by account. The editor offers the Overage label, since it has no usage data, and an edited label replaces whichever one the account would show.
Upstream's label editor (sirmalloc#601) gives each widget one default label, while Extra Usage Used, Remaining and Utilization pick "Overage" or "Spend" by account. The editor offers the Overage label, since it has no usage data, and an edited label replaces whichever one the account would show.
Upstream's label editor (sirmalloc#601) drops a widget's label when its type changes, since the label names the old widget's value ("Model: " on Session Cost). It does that where the picker applies a type change, which this branch moves into placePickerSelection so the live preview and Enter share it. The label drop moves there too, so the preview shows the line without the old label, as Enter applies it.
…lors Upstream's label editor (sirmalloc#601) lets Thinking Effort's "Thinking: " be edited, but this branch draws the widget through formatThinkingEffort, which wrote the label itself. It now draws the edited label, or none, inside the brackets and in the widget color, and the default comes from one constant that getLabelPrefix shares. This branch also merges the label-key branch (#48): its (b)rackets key would otherwise clash with upstream's la(b)el… on a labeled widget.
…ication Brings in upstream main, including the label editor (sirmalloc#601). Where sirmalloc#601 meets this branch's shared bar code: - The bar widgets draw a bar through formatUsageBar, labeled with the widget's getLabelPrefix(item), so a bar mode keeps sirmalloc#601's bar label ("Block ", "Reset ") and an edited label. - Context % and Context % (usable) declare their label once, on ContextPercentageWidgetBase; the usage widgets on UsagePercentWidget.
Brings in upstream main, including the label editor (sirmalloc#601). This branch draws Model through formatModelName, which wrote "Model: " itself, so it now draws the edited label, or none, in the widget color; the default comes from one constant that getLabelPrefix shares.
…t/value-coloring Brings in Extra Usage Today with upstream main merged. Value colors draw their widget's label themselves, so with upstream's label editor (sirmalloc#601) formatColoredValue now draws the edited label, or none, in the widget color.
Extra Usage Today, Daily Budget, Extra Usage Limit, Session Cost Rate and Daily Cost Rate declare their default labels, so (e)dit label offers them as upstream's label editor (sirmalloc#601) does for its own widgets. The editor's default for Extra Usage Today and Limit is the "Overage" label, since it has no usage data; accounts without plan limits show "Spend". Each builds that label in one place for the editor and render. So "today: $12.34 of $50.00/day" is Extra Usage Today labeled "today: ", Daily Budget labeled "of " and a "/day" Custom Text, with no raw values.
With the label editor (sirmalloc#601) in main, anyone on a usage-based Enterprise plan can relabel the Extra Usage widgets' "Overage" to "Spend" in a few keys, so detecting those accounts from the usage API's response shape isn't worth its cache field and heuristics. What stays is the wording: the three widgets' descriptions say they cover Pro/Max overage and Enterprise spend, the docs say the same and point at the label editor, and picker searches for "enterprise" or "spend" list the three widgets first, which a new test checks.
Conflict resolutions: - Label override (upstream sirmalloc#601): fork widgets keep their own rendering and read the label through getLabelPrefix/getLabel, so per-item label overrides apply. Raw mode keeps the fork's metadata.prefix. - Usage percent widgets (upstream sirmalloc#545 moved them onto a shared module): the fork's SessionUsage, WeeklyUsage, WeeklyOpusUsage and WeeklySonnetUsage keep their own implementations (pace indicator, monthly/weekly promotion, cursor), so they do not take label overrides. FableWeeklyUsage, unchanged in the fork, uses the upstream shared module. - BlockResetTimer keeps the "Weekly Reset" label when weekly is promoted into the session slot, now derived from the mode-aware upstream label. - ContextLength keeps the fork's resolveContextLengthTokens fallback instead of the upstream tokenMetrics branch. - MonthlyUsage now honors the configured percent number format, which the shared upstream usage widget test suite checks.
…s too (#670) * feat: label Extra Usage as spend on accounts without plan limits On usage-based plans (Claude Enterprise) the usage API reports every rate-limit window as null and no limits[] entries: there's no plan limit to go over, and extra usage is the account's whole spend. The Extra Usage widgets still called it overage ("Overage Used: $123.45"), and their picker descriptions only mentioned pay-as-you-go overage, so Enterprise users had no reason to think these widgets were theirs. Searching the picker for "enterprise" or "spend" didn't find them. The parser now flags a response that explicitly reports no plan limits (`noPlanLimits`). The flag is kept in the usage cache and through the merge with statusline rate_limits, and the three widgets label their value "Spend Used", "Spend Left" and "Spend" when it's set. Only an explicit null counts and any limits[] entry counts as a limit, so every uncertain case keeps the "Overage" label. A cache written before this change keeps the old label until its next refresh. The widget descriptions now mention both Pro/Max overage and Enterprise spend, which also makes the picker's search find them. * test: cover the Spend label with an edited label Upstream's label editor (#601) gives each widget one default label, while Extra Usage Used, Remaining and Utilization pick "Overage" or "Spend" by account. The editor offers the Overage label, since it has no usage data, and an edited label replaces whichever one the account would show. * refactor: keep the Enterprise wording, drop the Spend label detection With the label editor (#601) in main, anyone on a usage-based Enterprise plan can relabel the Extra Usage widgets' "Overage" to "Spend" in a few keys, so detecting those accounts from the usage API's response shape isn't worth its cache field and heuristics. What stays is the wording: the three widgets' descriptions say they cover Pro/Max overage and Enterprise spend, the docs say the same and point at the label editor, and picker searches for "enterprise" or "spend" list the three widgets first, which a new test checks. * test: check the picker lists the Extra Usage widgets first, however many there are * test: look for the Extra Usage widgets among the widgets that mention the word * refactor(widgets): share Extra Usage Used and Remaining's code The two widgets were the same class apart from how the amount is worked out, and their label, name, description and preview sample. They now extend ExtraUsageAmountWidget, the way the Tokens widgets share TokenCountWidget. Rendering is unchanged.
Upstream: label prefixes editable with e (sirmalloc#601, sirmalloc#666), Session Cost Rate (sirmalloc#671) and Extra Usage Daily Budget (sirmalloc#672) widgets, own-lock-only usage lock clearing (sirmalloc#624), NO_PROXY and request deadlines, terminal control-sequence sanitizing, git hardening and linear-time parsing, shared searchable option editor (sirmalloc#649) and widget base classes (sirmalloc#646-sirmalloc#654), node: imports (sirmalloc#657), Number.* globals lint (sirmalloc#645), test waits on redraw instead of fixed delays (sirmalloc#652). Hand edits outside conflicts: - src/widgets/shared/searchable-option-editor.tsx: compat fix - ports the fork's ref-based input handling (Enter picks from what was just typed) into upstream's new shared editor - src/utils/usage-fetch.ts: compat fix - fork's fetchFreshUsageData uses upstream's clearOwnUsageLock (sirmalloc#624) instead of the removed clearUsageLock - src/widgets/ApiUsage.tsx, src/widgets/Battery.ts: compat fix - Number.isNaN/Number.parseInt for upstream's no-restricted-globals rule - src/utils/__tests__/usage-prefetch.test.ts: compat fix - mock renamed to upstream's mockFetchUsage - src/tui/components/__tests__/PowerlineSetup.test.ts, src/tui/components/__tests__/StatusLinePreview.test.ts, src/tui/components/__tests__/TerminalWidthMenu.test.ts: compat fix - drop the fork's flushInk import, unused after upstream's waitFor switch - README.md: upstream notes linked docs/USAGE.md (deleted in the fork); inlined the proxy and workspace-trust text instead; upstream's own release-note entries not taken - .fork-keep-deleted: src/widgets/FableWeeklyUsage.ts (upstream widget, unregistered in the fork, which ships WeeklyFableUsage.ts) - src/widgets/WeeklyFableUsage.ts: compat fix - adopts upstream's shared getUsagePercentWidgetEditorDisplay, which carries the time-cursor modifier - src/tui/__tests__/App.test.ts: fork deviation - one extra down-arrow for the fork's Off Hours main-menu row - src/widgets/__tests__/CacheTimer.test.ts, src/widgets/CacheTimer.ts: compat fix - emoji toggle moved from e to o because upstream reserves e for edit-label - src/widgets/AgentAddress.ts: split the session-title guard into its own if (same behaviour) so the .fork-invariants nameSource guard matches again - docs/test-retirements.md: ledger for tests upstream renamed or replaced
Seeing what a widget looks like used to mean adding it, looking, and removing it again. While the add/insert/change picker highlights a widget, the status line preview now shows the line exactly as Enter would leave it; ESC or browsing categories restores the line. The new widget's id and powerline background are fixed when the picker opens, so the preview doesn't change color on every keypress and Enter adds exactly what was shown. Preview and apply share one placement helper. A type change drops the widget's label there, as the label editor (sirmalloc#601) does, so the preview shows the line without the old label, as Enter applies it. The picker rendered every entry, so the All category (~90 widgets) or a broad search ran past the bottom of the terminal and scrolled the preview off the top. Picker lists now show only the entries around the selection, sized to the terminal height, the number of status lines and the list's tallest description (wrapped at the terminal width as Ink wraps it), with "↑ N more" / "↓ N more" markers. Entry numbers stay absolute, so position in the full list is still visible.
* feat(tui): preview the highlighted widget live while picking Seeing what a widget looks like used to mean adding it, looking, and removing it again. While the add/insert/change picker highlights a widget, the status line preview now shows the line exactly as Enter would leave it; ESC or browsing categories restores the line. The new widget's id and powerline background are fixed when the picker opens, so the preview doesn't change color on every keypress and Enter adds exactly what was shown. Preview and apply share one placement helper. A type change drops the widget's label there, as the label editor (#601) does, so the preview shows the line without the old label, as Enter applies it. The picker rendered every entry, so the All category (~90 widgets) or a broad search ran past the bottom of the terminal and scrolled the preview off the top. Picker lists now show only the entries around the selection, sized to the terminal height, the number of status lines and the list's tallest description (wrapped at the terminal width as Ink wraps it), with "↑ N more" / "↓ N more" markers. Entry numbers stay absolute, so position in the full list is still visible. * refactor(tui): find the highlighted picker category with indexOf The category list holds plain strings, so finding the highlighted one needs no callback; a picker with no categories has none highlighted.
* feat: label Extra Usage as spend on accounts without plan limits
On usage-based plans (Claude Enterprise) the usage API reports every
rate-limit window as null and no limits[] entries: there's no plan limit
to go over, and extra usage is the account's whole spend. The Extra
Usage widgets still called it overage ("Overage Used: $123.45"), and
their picker descriptions only mentioned pay-as-you-go overage, so
Enterprise users had no reason to think these widgets were theirs.
Searching the picker for "enterprise" or "spend" didn't find them.
The parser now flags a response that explicitly reports no plan limits
(`noPlanLimits`). The flag is kept in the usage cache and through the
merge with statusline rate_limits, and the three widgets label their
value "Spend Used", "Spend Left" and "Spend" when it's set. Only an
explicit null counts and any limits[] entry counts as a limit, so every
uncertain case keeps the "Overage" label. A cache written before this
change keeps the old label until its next refresh.
The widget descriptions now mention both Pro/Max overage and Enterprise
spend, which also makes the picker's search find them.
* refactor: share the cache file lock
Move the terminal width cache's best-effort O_EXCL lock into its own
module so other cache files can use the same read-modify-write
protection instead of copying it. No behavior change.
* feat: add a daily state file for per-day usage tracking
Some usage figures are per UTC day, but ccstatusline only ever sees the
current month-to-date totals. Keep a small state file next to the other
caches that records what was seen and when, modeled on the terminal
width cache: one JSON file, the shared lock around each
read-modify-write, an atomic temp-file rename, pruning to today and
yesterday, and injected file and clock dependencies.
* feat: track each day's starting extra usage spend
The usage API reports extra usage spent month to date only. Record the
total per login in the daily state file and work out today's spend from
where it stood when the UTC day began: the last value seen the previous
day, zero on the 1st (extra usage resets at 00:00 UTC), or otherwise the
first value seen today. A total that drops mid-day never makes today's
spend negative, and an observation that loses the state lock to another
process still reports today's spend.
* feat: work out today's extra usage spend during the usage prefetch
fetchUsageData now remembers the token fingerprint of the login it
used. When a line shows today's spend, the prefetch records the
month-to-date total for that login in the daily state file and adds
extraUsageUsedToday (cents) to the usage data. It's worked out on every
render, cache hits included, and never stored in the usage cache.
* feat: add an Extra Usage Today widget
Shows the extra usage spent since 00:00 UTC, including claude.ai and
other machines, labeled like its siblings: "Spend Today" on accounts
without plan limits, "Overage Today" otherwise. It shows nothing until a
usage fetch has run since the UTC day began, and n/a when extra usage
is off, with the same hide options, currency and cost number format as
the other Extra Usage widgets.
* fix: count a cached extra usage total for the day it was fetched
Today's spend recorded every usage prefetch as an observation at render
time, cache hits and stale fallbacks included. A total fetched at 23:58
UTC on the last day of the month and replayed from the cache at 00:01
became the 1st's first observation, so Spend Today showed the whole
previous month for up to three minutes, or for as long as the API stayed
unreachable.
fetchUsageData now reports when its data came from the API
(getUsageFetchedAt: now for a fresh fetch, the cache file's write time
for a cache hit or stale fallback), and the observation is recorded at
that time. A total fetched before midnight updates the previous day's
record, so today's spend shows nothing until a total fetched today
arrives, and an older fetch replayed after a newer one is ignored.
* test: check fetchUsageData reports the login and fetch time
Today's extra usage spend depends on fetchUsageData recording which
login and which fetch its data came from, and every other test replaces
that link with a spy. Run the real fetch in the probe harness and check
it reports the login fingerprint the usage cache is written with and the
API fetch time, and neither when there are no credentials.
* test: cover the Spend label with an edited label
Upstream's label editor (#601) gives each widget one default label,
while Extra Usage Used, Remaining and Utilization pick "Overage" or
"Spend" by account. The editor offers the Overage label, since it has
no usage data, and an edited label replaces whichever one the account
would show.
* refactor: keep the Enterprise wording, drop the Spend label detection
With the label editor (#601) in main, anyone on a usage-based Enterprise
plan can relabel the Extra Usage widgets' "Overage" to "Spend" in a few
keys, so detecting those accounts from the usage API's response shape
isn't worth its cache field and heuristics.
What stays is the wording: the three widgets' descriptions say they
cover Pro/Max overage and Enterprise spend, the docs say the same and
point at the label editor, and picker searches for "enterprise" or
"spend" list the three widgets first, which a new test checks.
* test: check the picker lists the Extra Usage widgets first, however many there are
* test: look for the Extra Usage widgets among the widgets that mention the word
* refactor(widgets): share Extra Usage Used and Remaining's code
The two widgets were the same class apart from how the amount is worked
out, and their label, name, description and preview sample. They now
extend ExtraUsageAmountWidget, the way the Tokens widgets share
TokenCountWidget. Rendering is unchanged.
* fix: drop extra usage times stamped while the clock ran ahead
Extra Usage Today ignores a fetch older than the one it last recorded,
so a cached fetch a concurrent render replays late can't move the day
back. A single render made while the system clock ran ahead defeats
that: it records a fetch stamped at a time that hasn't come yet. Once
the clock is put right, every real fetch looks older and is ignored, so
Today disappears for the rest of the day; and when the clock reaches the
stamped time, that record is taken as the day's, with the previous
day's total as its start. With $10 seen at 09:00, one render a day
ahead, then $25 and $40 later that day, Today was hidden for the rest of
the day and showed $40 the next day for at most $10 of real spend.
The usage cache that render wrote is stamped by the same clock and is
served as fresh until the clock catches up with it, so its fetch keeps
arriving with the future time and would store the same record again.
A record last seen more than a minute ahead of now is now dropped and
the day starts afresh, and a fetch stamped more than a minute ahead
isn't recorded. The minute's leeway keeps a small step back of the
clock, such as an NTP correction just after a fetch, from dropping the
day's starting point. Now is read after the state file, so a fetch
another render has just recorded is never taken for one from the
future and the replay guard still holds.
* fix(usage): remove daily.json's temp files when a write fails
updateDailyState writes daily.json through a temp file named
daily.json.<pid>.tmp and renames it into place. When the write or the
rename failed, for example a write cut short by a full disk or a rename
refused with EPERM while a Windows virus scanner or sync client holds
daily.json, the error went to the catch-all and the temp file stayed
behind. Every status line render is a new process with a new pid, so
each failing render left another orphan in ~/.cache/ccstatusline, and
nothing ever deleted them.
A failed write or rename now unlinks the temp file. A failed unlink is
ignored; as before, the state stays best-effort and the render goes on.
The same handle can be held on the temp file itself, and then it
refuses the unlink as well as the rename. So each write also removes
the other daily.json.<digits>.tmp files next to the state file once
they are a minute or more old, which no running write can still own.
While a handle stays held, only the last minute's temp files remain,
and orphans an older build left behind are cleared as well. The temp
name stays per process, so two renders never write through the same
file.
* fix(usage): keep today's extra usage spend across a token refresh
Extra Usage Today keeps each login's month-to-date total at the start of
the UTC day in daily.json, keyed by the usage cache's token fingerprint,
a hash of the refresh token. Claude Code saves a new refresh token each
time it refreshes the access token, which happens several times on a
working day. The key then changed mid-day, the new key had no record,
and today's spend started over from that moment: $15.00 at 13:00, then
$0.00 at 17:00 instead of $35.00 and $10.00 at 18:00 instead of $45.00.
The old record stayed behind until it was pruned.
The record is now kept under the account and organization the
credentials belong to. While a widget shows today's spend, ccstatusline
asks /api/oauth/profile through the usage fetch's own request, proxy
and credential code, once per token fingerprint, and keeps the answers
in ~/.cache/ccstatusline/usage-account.json. A failed lookup holds off
that fingerprint alone, for 30 seconds or a 429's Retry-After (at most
a day), and the hold-off is written before the request goes out, so
renders running side by side send one request between them.
Until the lookup answers, the record stays under the fingerprint as
before. Once the account is known, the records under its fingerprints,
written by an earlier build or while the lookup was failing, are folded
into the account's record once, so the day keeps its starting total.
A record seen while the clock ran ahead is dropped instead, as it would
be under its own key. A fingerprint that was never looked up can't be
placed with an account, so today's spend still starts over once when
an earlier build kept the record under a refresh token that has since
changed, or when a lookup outage spans a refresh.
The organization is part of the key because one account can sign in to
a personal organization and a work one, each with its own extra usage
total. The key is a hash, so neither cache file names the account.
* refactor(usage): tidy the account lookup and the record merge
mergeSpendRecord goes through an arrow, so reduce's index and array
arguments never reach it, and the account id is built before it's
fingerprinted instead of inside a nested template literal.
* refactor(usage): ask the profile endpoint once for the account email and the account key
The Claude Account Email widget (#724) and Extra Usage Today each asked
/api/oauth/profile about the same login, each with its own cache and backoff.
One request now answers both. lookUpProfile runs under the email lookup's
per-login lease and backoff files, and each caller keeps its part of the
answer in its own cache: the email in account-email-<hash>.json, the account
key in usage-account.json, which now holds only named logins. An answer that
leaves out the caller's part holds that login off as before, and the other
part is still kept.
A rate-limited lookup's Retry-After is kept within the day the backoff reader
trusts, as the account lookup did, so the email lookup no longer reads a
Retry-After further off as no backoff at all.
* docs(development): the account email and Extra Usage Today share one profile lookup
* refactor(usage): name the account id before fingerprinting it
The account key nested the id's template inside its own on one line. Build
the id first, as the account lookup did before it shared the profile
request.
* fix: enable label editing for Extra Usage Today
---------
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
Closes #600
Adds a per-widget label override for widgets that show a text label (
Model:,Ctx:,In:, …). Select the widget in the line editor and pressb(la(b)el…).Changes
metadata.labeland used exactly as typed. An empty string removes the label. A value equal to the current default is still stored, so it applies in every mode (see below). Changing a widget's type drops its label. No schema change or migration.formatRawOrLabeledValuenow uses the override, so every widget that already went through it gets the feature from one change. Each widget'srendertakes its default from its owngetLabelPrefix(item), so the label the editor offers and the one rendered can't drift apart. Widgets that built their label inline now use the helper too: Model, Session ID, Account Email, Context Length/Window/Bar, Memory Usage, Output Style, Session Clock/Name/Cost, Thinking Effort, Terminal Width, Git Staged/Unstaged/Untracked Files, and Current Working Dir (its optional glyph still comes before the label).Widget.getLabelPrefix(item), which returns their default label for the item's current mode (Context % givesCtx Used:orCtx Left:; Block, Block Reset and Weekly Reset timers giveBlock,ResetandWeekly Resetin their bar modes). The usage and speed families get theirs from their shared config.Block:vsBlockbefore a bar) use the same override in all modes, even one that matches the current mode's default (e.g.Blockset in a bar mode also drops the colon in time mode).ItemsEditor, the same way(h)ide…is, and is hidden while raw value is on.LabelEditor. The value is shown in quotes so trailing spaces are visible. Enter saves the typed label and Tab clears the override back to the default.(label: "M ")while an override is set. The label is JSON-escaped there and in the editor's default, so a"inside it reads clearly.lwould be the natural key, but the reset timers already use it for(l)ocale.useTextCursor(src/widgets/shared/text-cursor.ts), which the label editor and Custom Text now both use. This also makes Ctrl+←/→ jump to the start/end as Custom Text's hint says. The old checks comparedinputto'ArrowLeft', which Ink never sends, and ran after the plain-arrow branch, so they never matched. Edits apply through a functional state update so keys typed before a re-render aren't lost, and the cursor ends with\x1b[27m(inverse off) rather than a full reset.getGraphemesis exported and replaces the copies in Custom Symbol and the symbol override editor.(g)lyph. Widgets whose prefix is really part of the value (Version, Git PR, Sandbox/Voice/Remote Control status, Git Is Fork, Link) aren't changed.Reset [Loading]/Weekly Reset [Loading]instead ofReset: [Loading].docs/USAGE.mdcovers the keybind in the raw value section and in the editor keybind list.Testing
getLabelPrefix(item)bitself in any mode, next to the existinghcheckItemsEditortest coveringb→ edit → save →(label: "M "), with the keybind and row tag hidden in raw value mode. Further editor tests cover: a type change dropping the label; two keys typed before a re-render both landing; a joined-emoji label shown whole beside its default; and a bar-modeBlocklabel being saved, then cleared with Tabbun testandbun run lintpasssettings.json
{ "version": 4, "lines": [ [ { "id": "1", "type": "model", "metadata": { "label": "M " } }, { "id": "2", "type": "separator" }, { "id": "3", "type": "model" }, { "id": "4", "type": "separator" }, { "id": "5", "type": "terminal-width", "metadata": { "label": "" } } ] ] }🤖 Generated with Claude Code