Skip to content

feat(widgets): value colors on the usage, context, memory, remaining and cache hit widgets - #46

Draft
eric-engberg wants to merge 115 commits into
feat/value-coloringfrom
feat/value-colors-more
Draft

eric-engberg wants to merge 115 commits into
feat/value-coloringfrom
feat/value-colors-more

Conversation

@eric-engberg

@eric-engberg eric-engberg commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Based on #41 (value colors), and merges #43 (level glyph), whose glyph mode shares these widgets: review the four non-merge commits after those merges. It goes upstream after #41 and #43.

What

Value colors (v) come to every widget that shows a number with a natural 100%:

  • Usage percents: Block Usage, Weekly Usage, Weekly Sonnet, Weekly Opus, Weekly Fable and Spend Limit Usage, in percentage mode.
  • Context: Context % and Context % (usable), in percentage mode, and Context Length.
  • Others: Memory Usage, Extra Usage Remaining, and Cache Hit Rate.

Each one opens the same editor as Extra Usage Utilization: break points or a gradient, colors, and the 256-color warning.

Why

Value colors arrived with the Extra Usage widgets (#39, #41). Weekly Fable Usage sitting next to them, in plain colors, showed the gap: the usage and context percents are exactly the values you want to read at a glance. I went through every widget that shows a number. This PR covers the ones measured against a whole: a limit, the context window, total memory, or a hit rate's 100%. Costs, rates and token counts, which need break points in their own units, are left for a follow-up.

How

  • Same break points as Utilization for the usage widgets, Context % and Extra Usage Remaining: green below 70% used, yellow below 90%, red from 90%. They follow the used percent even while a widget shows what's left.
  • How each of the rest measures:
    • Context Length against the context window, as Context % does;
    • Memory Usage by the share of memory used;
    • Extra Usage Remaining by the share of the limit spent, as Extra Usage Used does.
  • Cache Hit Rate counts the other way. A scale can now set higherIsBetter, which makes its bands default to red, yellow and green and runs a gradient from its urgent end. Its break points are 50% and 80%.
  • Percentage mode only. On the usage and Context % widgets, bars keep their gradients and the level glyph (feat(widgets): show a percent widget's level as a glyph #43) has no number to color, so v, the value colors modifier and preservesRenderedColors apply only to the plain percent. Two small helpers carry that rule, showsPlainUsageValue and withValueColorsKeybind, and Utilization now uses them too.
  • Where the code lives: the usage widgets get it once, through their shared base class; Context % and Context % (usable) through theirs. The editor's sample is drawn in each widget's own default color, so Spend Limit Usage's shows its magenta rather than the other usage widgets' blue.
  • Nothing changes until value colors are turned on. Off, every widget draws exactly the text it did before.

Demo

Piped through the real pipeline with a fake login, a fresh usage cache and a payload, at three levels. All amounts are made up except Memory Usage, which is this machine's. The usage, context and spend levels rise from 40% to 95%, while the cache hit rate falls from 92% to 30%.

Powerline

Value colors on more widgets, Powerline

Plain

Value colors on more widgets, plain

The editor: v on Weekly Fable Usage turns value colors on. Then Cache Hit Rate's editor shows its reversed defaults (low red, high green, 50% and 80%), and both rows end up marked (value colors).

Powerline

Value colors editor on Fable and Cache Hit Rate, Powerline

Plain

Value colors editor on Fable and Cache Hit Rate, plain

Fix: labels and empty values under a theme or item color

With value colors on, these widgets colored their label in the widget's own color and left text with no value to color in the terminal's default color. Under Powerline with the solarized theme, Context %'s Ctx Used: came out blue on the theme's blue, and Cache Hit: n/a ignored the theme's text color. In plain mode, a gradient item color on Context % showed only its first stop, and Cache Hit: n/a ignored its cyan item color. Now the value keeps its value color and the rest takes the theme's text color, the item color or the gradient. Piped renders at 45%, 75% and 92% context with no cache data; the payloads are sample data.

Powerline (solarized theme, 256 colors)

Before:

Value colors under the solarized theme before the fix: Ctx Used in blue on blue, n/a in the default color

After:

Value colors under the solarized theme after the fix: the labels and n/a in the theme's text color

Plain (Context % with item color gradient:atlas, Cache Hit Rate cyan)

Before:

Value colors with item colors before the fix: Ctx Used in the gradient's first stop, n/a uncolored

After:

Value colors with item colors after the fix: the gradient sweeps Ctx Used, n/a in cyan

Testing

  • Tests, each written first and seen failing:
    • usage-widget-suites.ts, run for all five usage widgets (4 new each, 20 in all): bands at 69.9/70/89.9/90%; the used percent while showing what's left, with the label in the widget color; bar and glyph modes unchanged and without v; and the modifier, editor and preservesRenderedColors. The shared keybind test now expects v on the plain percent.
    • context-value-colors.test.ts (10):
      • Context % and Context % (usable): the preview, showing what's left, the slider and glyph modes, and the keys and editor;
      • Context % live at 69.9/70/90%;
      • Context Length against the window, with the label and with value colors off.
    • more-value-colors.test.ts (8): Memory Usage, Extra Usage Remaining at 20/70/90% spent and with a zero limit, Cache Hit Rate at 49/50/80%, and its editor's red, yellow and green defaults.
    • value-coloring.test.ts (2 new): a higher-is-better scale's default bands and its reversed gradient.
    • CacheWidgets.test.ts: Cache Hit Rate's keys now include v.
  • Bun: bun test gives 3055 pass, 0 fail (Bun 1.4.2). bun run lint is clean. A 12-run loop of the editor Ink tests had no failures.
  • Node 26 vitest: the 12 touched test files pass (226 tests).
  • Built CLI on both runtimes: runtime-check.sh -c value-colors-more with seeded data showed Powerline output agreeing. Plain output differed once, because Memory Usage reads live memory and it changed between the Bun and Node runs. With Memory Usage left out, Bun 1.4.2 and Node 26.10.0 agree byte for byte. The TUI opens and exits under both.

Setting a widget's glyph meant typing it, which needs a way to type
emoji or Nerd Font icons. In the glyph editor, → now opens a picker:

- 17 groups of glyphs, paged with Tab/Shift+Tab or PgDn/PgUp: folders
  and files, dev tools, git and status, weather and time, nature,
  objects, faces and hearts, arrows, shapes, text symbols, then Nerd
  Font groups (folders, git, languages, OS and apps, status, weather)
  and Powerline glyphs.
- The arrow keys move through a group's grid, the highlighted glyph's
  name shows below it, Enter picks it and ESC goes back.

Each group's glyphs all draw at one width so the grid lines up: emoji
that terminals show two cells wide by default, and one-cell symbols and
Nerd Font icons. Names come from Unicode and the Nerd Fonts glyph names.
Typing a glyph still works.
The percent widgets (Context %, Context % (usable), Session and Weekly
Usage, Extra Usage Utilization) get one more style for (p): a single
glyph for how high the used percent runs, 🟢 under 20%, ⚡ from 20%, 🔥
from 70% and 🚨 from 90%. It replaces the number; a second copy of the
widget merged beside it shows both, e.g. `⚡ 42.0%`.

The glyph always measures what's used, so (u) isn't offered in this
mode. (g) sets the glyph for each level in the glyph editor, picker
included, and (l) the three break points. Settings store only what
differs from the defaults: `levelGlyph<Level>` and `levelFrom<Level>`.
A row's glyphs are unique within its group, so they make a stable key
where the row's position doesn't.
The curated groups hold about 280 of Nerd Fonts' 10,600 icons, so many,
like the pull request created, merged, closed and draft icons, weren't
there. Typing in the picker now searches every glyph by name: the
curated groups' and all of Nerd Fonts', matching every word of the
search (`pull req draft`). The groups stay for browsing; a long list of
matches scrolls.

The full set comes from Nerd Fonts' glyph names
(`scripts/generate-nerd-font-glyphs.ts` regenerates it), with aliases
sharing a codepoint merged into one entry. The picker loads all its glyph
data when it opens, from its own build chunk, so the status line's
render path no longer loads even the curated groups.
The full Nerd Font set was only reachable by searching. After the 17
curated groups, the picker now has a group for each icon set: Codicons,
Devicons, Font Awesome, Material Design (6,896 icons), Octicons, Weather
Icons and the rest, 33 groups in all. PgUp/PgDn move a page within a
group and Tab/Shift+Tab switch groups (PgUp/PgDn switched groups
before).
A break point held one above its neighbor, or typed, kept its odd offset
on every later step (21, 26, 31...), so it never landed on a multiple of
5 again. Steps now go to the next multiple of 5 in that direction.
The arrow and paging keys now work out where the highlight goes in one
function, so the key handler only decides between picking, going back,
moving and typing. The Nerd Font name format builds its alias list
first, and the generator script reads glyphnames.json from the current
directory instead of taking a path.
The picker's search found every Nerd Font icon but only the curated
emoji and symbols, so most emoji (🤡 clown, 🤢 nauseated) couldn't be
found at all. It now also searches every fully-qualified Unicode 18.0
emoji, without the skin tone variants, and every character in the
Unicode symbol blocks (arrows, box drawing, math, dingbats and more),
plus a few common ones from elsewhere (§ ± × ÷ π ...).

scripts/generate-unicode-glyphs.ts generates the data from Unicode's
emoji-test.txt and UnicodeData.txt, as the Nerd Font data is generated
from glyphnames.json. It loads with the rest of the picker's data, so
the status line still never loads it. A glyph is listed once, under its
first name, and found by any of its names; an emoji's text and emoji
forms (❤ and ❤️) stay separate glyphs.
Ctrl+T in the glyph picker cycles the emoji skin tone: the default, then
light, medium-light, medium, medium-dark and dark. Every emoji that takes
a tone (332 of them) shows and picks in it, in the groups and in search,
with both people toned in a two-person emoji. The tone is a new optional
setting, emojiSkinTone, so it's remembered next time; an unknown value
falls back to the default rather than invalidating the settings.

The Unicode generator records each such emoji's light tone version,
worked out from Unicode's own toned versions (the emoji selector goes
when a tone comes in, and both people take it), keeping only emoji that
have all five tones. The picker turns that into the chosen tone. It
reads and saves the tone through a small context that the line editor
provides, so the widget editors in between don't change.

The glyph editor now shows a multi-part emoji whole (👍🏾, 👩‍💻). Ink lays
out each part as its own character, so the "(default: …)" text after it
overwrote the tone or the rest of the emoji; the two are one Text now.
A new object on every app render made everything reading the context
redraw with it; the value is memoized on the tone now.
…ue-colors-more

# Conflicts:
#	docs/USAGE.md
#	src/widgets/ExtraUsageUtilization.ts
#	src/widgets/__tests__/ExtraUsageUtilization.test.ts
A scale can mark a higher value as the good one, as a cache hit rate is:
its bands then default to red, yellow and green, and a gradient runs from
its urgent end, so 100% is the calm color.
…h they run

Session Usage, Weekly Usage, Weekly Sonnet, Weekly Opus and Weekly Fable
get value colors (v) on their plain percent, with Extra Usage
Utilization's break points: green below 70% used, yellow below 90%, red
from 90%, following the used percent even while they show what's left.
Their bar modes keep their gradients and the level glyph has no number
to color, so (v) is offered only on the plain percent. Utilization now
uses the same helpers for when that is.
…text is

Context % and Context % (usable) get value colors (v) on their plain
percent, with the usage widgets' break points, following the used
percent while they show what's left; the slider and the level glyph
keep their own looks, so (v) is offered only on the plain percent.
Context Length gets them too, measured against the context window as
Context % is: 150k of a 200k window is yellow.
…t Rate

Memory Usage colors by the share of memory used, and Extra Usage
Remaining by the share of the limit spent, as Extra Usage Used does,
both with the usage widgets' break points. Cache Hit Rate counts the
other way, since a higher rate is the good one: red below 50%, yellow
below 80%, green from 80%. The docs list every widget with value
colors and how each one measures.
# Conflicts:
#	src/widgets/shared/symbol-override.tsx
# Conflicts:
#	src/widgets/ExtraUsageUtilization.ts
#	src/widgets/shared/context-percentage-widget-base.ts
#	src/widgets/shared/symbol-override.tsx
# Conflicts:
#	src/widgets/CacheHitRate.ts
#	src/widgets/ContextLength.ts
#	src/widgets/FreeMemory.ts
#	src/widgets/shared/context-percentage-widget-base.ts
#	src/widgets/shared/symbol-override.tsx
eric-engberg added a commit that referenced this pull request Oct 30, 2026
…colors

Brings in upstream main through #46. #72 is upstream as sirmalloc#699, so the
files this branch had from it take main's versions, and only Block
Forecast's value colors stay on top: its widget, its tests and the docs
lines for (v). Main's BlockForecast.ts matches the #72 code this branch
built on, so the value colors apply to it unchanged.

# Conflicts:
#	docs/DEVELOPMENT.md
#	docs/USAGE.md
#	src/types/RenderContext.ts
#	src/utils/__tests__/usage-fetch.test.ts
#	src/utils/usage-fetch.ts
#	src/utils/usage.ts
#	src/widgets/BlockForecast.ts
#	src/widgets/__tests__/BlockForecast.test.ts
eric-engberg added a commit that referenced this pull request Oct 30, 2026
Brings in upstream main through #96. The conflicts are between main and
the #46 code this branch holds; those files take the resolutions #46 made
when main reached it, with #96's paste handling on top where #96 changes
them.

# Conflicts:
#	docs/DEVELOPMENT.md
#	docs/USAGE.md
#	src/tui/App.tsx
#	src/tui/__tests__/App-dev-reload.test.ts
#	src/tui/components/ItemsEditor.tsx
#	src/tui/components/__tests__/ItemsEditor.test.ts
#	src/types/RenderContext.ts
#	src/utils/__tests__/usage-fetch.test.ts
#	src/utils/__tests__/usage-prefetch.test.ts
#	src/utils/renderer.ts
#	src/utils/usage-fetch.ts
#	src/utils/usage-prefetch.ts
#	src/utils/usage-types.ts
#	src/utils/usage.ts
#	src/widgets/SessionUsage.ts
#	src/widgets/__tests__/ThinkingEffort.test.ts
#	src/widgets/shared/symbol-override.tsx
#	src/widgets/shared/usage-display.ts
#	src/widgets/shared/usage-percent-widget.ts
# Conflicts:
#	docs/USAGE.md
↑ and ↓ each spelled out the wrap-around in the editor's key handler,
which the picker key and the Shift+Tab toggle had already made long. One
helper keeps it short; a test now covers the wrap in both directions.
The level glyph stays the last style (p) cycles to, now after the line:
text, block, slider, line, glyph on the percent widgets, and percentage,
slider, line, glyph on Context %. The timers still go from the line back to
text.
@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants