Skip to content

feat(thinking-effort): optional brackets and per-level colors - #777

Merged
sirmalloc merged 3 commits into
sirmalloc:mainfrom
eric-engberg:feat/thinking-effort-brackets-colors
Nov 6, 2026
Merged

sirmalloc merged 3 commits into
sirmalloc:mainfrom
eric-engberg:feat/thinking-effort-brackets-colors

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

Demo

Model is merged with Thinking Effort, so the preview reads Claudehigh (the TUI's sample data). Pressing b adds brackets, giving Claude(high). Pressing l opens the level colors editor: Space turns level colors on, and moving through the rows shows each level's color in the sample. Enter saves, and the preview colors the level.

Powerline

Thinking Effort brackets and level colors in Powerline mode

Plain

Thinking Effort brackets and level colors in plain mode

Fix: the label's color with level colors on

With level colors on, the Thinking: label kept the widget's own color whatever the line's colors were: magenta on the theme's teal under Powerline with the nord theme, and only the first stop of a gradient item color. It now takes the theme's text color, or the gradient sweeps across it, and the level keeps its level color. Piped renders at high, xhigh and max; the payloads are sample data.

Powerline (nord theme, 256 colors)

Before:

Thinking Effort under the nord theme before the fix: the label in magenta on teal

After:

Thinking Effort under the nord theme after the fix: the label in the theme's text color

Plain (item color gradient:atlas)

Before:

Thinking Effort with a gradient item color before the fix: the label in the gradient's first stop

After:

Thinking Effort with a gradient item color after the fix: the gradient sweeps the label

Update

Status line rendered from a fake payload at the 256 Color level, with Thinking Effort's level colors set to the hex colors #f5d020, #ff8800 and #e8343c. Under each line is the escape code the real output uses for the level. Before the fix these are 24-bit codes (ESC[38;2;255;136;0m), which 256-color terminals can't show. After it they are the nearest 256-palette codes (ESC[38;5;220m, 208, 167). VHS's terminal supports truecolor, so the rendered colors only shift by that rounding, and max's red shifts the most.

Powerline, before and after:

Before, Powerline

After, Powerline

Plain, before and after:

Before, Plain

After, Plain

Summary

Showing the effort next to the model, e.g. Opus 5.5(xhigh) with the level color-coded, used to take four merged widgets (Model, (, Thinking Effort, )) and still couldn't color by level.

Thinking Effort gains two options. Both are off by default, so existing setups render exactly as before.

  • (b)rackets cycles off → () → [] → {} → <> and wraps whatever the widget renders: raw gives (xhigh), labeled gives (Thinking: xhigh).
  • (l)evel colors opens an editor that colors the value by level:
Thinking Effort: level colors
↑↓ select, ←→ change color, Space on/off, (x) custom color, (d)efaults, Enter save, ESC cancel

Sample: (high)

Level colors: On

   low        Bright Black
   medium     Green
▶  high       Yellow
   xhigh      Orange
   max        Red
   brackets   Match effort
  • The sample shows the highlighted level. On the brackets row it keeps showing the last level highlighted.
  • ←→ cycles through the named colors. (x) accepts #RRGGBB or 0-255; custom colors show as #RRGGBB or ANSI n, as in Edit Colors. (d) restores the defaults.
  • Brackets either match the level color, or use the widget's own color from Edit Colors.

Color responsibilities

  • Edit Colors keeps the fixed colors: the Thinking: label, unknown levels like super-max?, widget-colored brackets, background, and bold/dim. It now shows a one-line note when the selected widget sets some of its own colors.
  • The level-colors editor sets only the colors that change with the level.
  • When level colors are on, the widget paints only the level (and brackets that match it), and the renderer colors the rest the way it colors any widget: the Powerline theme's foreground, the widget color, or a gradient swept across the uncolored text. A new optional Widget.colorsOnlyItsRuns() marks such widgets; the renderer returns to its color after each run the widget colored. Claude Status and Custom Command, which own all of their foreground, are unchanged.
  • Color level 0 outputs plain text, and a global foreground override still strips the colors, as the renderer already does for these widgets.
  • The default xhigh orange (ansi256:208) falls back to bright magenta on 16-color terminals. Colors you pick yourself are left as-is, the same as the color menu.
  • The widget color follows the renderer's rules: unset means the widget default (magenta), and '' ("Default" in the color menu) means no color.

Implementation

  • widgets/shared/effort-style.ts: pure helpers for bracket cycling, level color get/set/reset, custom color parsing, and formatThinkingEffort(). Options are stored in widget metadata, and an option at its default stores nothing.
  • widgets/shared/effort-colors-editor.tsx: the editor screen.
  • ThinkingEffort.ts: keybinds, editor display ((brackets (), level colors)), and rendering through the formatter.
  • jsonl-metadata.ts: exports the existing KNOWN_THINKING_EFFORTS list so the levels have a single source.
  • ColorMenu.tsx: the note, shown for any listed widget that sets some of its own colors.
  • types/Widget.ts, utils/renderer.ts, utils/colors.ts, utils/gradient.ts: colorsOnlyItsRuns(), and coloring around the runs in the Powerline and plain paths (restoreForegroundAfterRuns, and a gradient that skips the runs).
  • The level-colors editor samples at the configured color level, so at 16 colors it shows the bright magenta xhigh the status line draws, and at No Color nothing.

Update: Map hex colors to the 256-color palette at the 256 Color level

  • At the 256 Color level, a hex: color now renders as the nearest 256-palette color (ESC[38;5;Nm / ESC[48;5;Nm). Before, it rendered as a 24-bit ESC[38;2;r;g;bm / ESC[48;2;r;g;bm code, while named, ansi256: and gradient colors already came out as palette codes. 256 Color is the default level, and Thinking Effort's level colors accept #RRGGBB at any level, so a hex color the user typed reached 256-color terminals (Terminal.app before macOS 26, for example) as an escape they can't draw. A separator that keeps its hex color after a switch from truecolor down to 256 goes through the same code, so it is fixed too.
  • The nearest color is chosen by squared RGB distance from palette entries 16-255: the 6x6x6 color cube at xterm's levels (0, 95, 135, 175, 215, 255) and the 24 grays. The 16 basic colors are left out because terminals theme them, so their real values are unknown. Hex colors don't use the gradient's rgbToAnsi256, which treats the cube levels as evenly spaced and can land well off. For #ff8800 it gives 214 (#ffaf00), where the search gives 208 (#ff8700, 1 unit away). For #5f5f5f, which is palette entry 59, it gives the gray 240.
  • Truecolor still gets the exact hex code. Basic and No Color still emit nothing for hex colors. Gradients are unchanged.

Testing

  • On main as of fix: use env override and settings for usable context ratio calculation #407 (71e8b42): merges cleanly, bun run lint is clean, and bun test gives 4006 pass, 0 fail. The built CLI under Bun 1.4.4 and Node 26.13.0 matches main in plain and Powerline mode with the baseline configs, and the TUI opens and exits cleanly. It adds no JavaScript or Node API newer than Node 14, the build's target.
  • New unit tests for the style helpers, widget rendering (brackets with raw on and off, each color mode, unknown levels, color level 0, preview, keybinds, editor display) and the editor (sample follows the cursor, color cycling both ways, bracket mode, on/off, custom color, invalid input, defaults, ESC discards), plus a ColorMenu test for the note.
  • bun test: 2429 pass. bun run lint: clean.
  • Build: the entry chunk is unchanged (20,552 bytes) and the shared chunk grew by about 3 KB.
  • Manual run in tmux:
    • add via the picker, set raw and brackets, turn level colors on and save: the preview shows (high) in yellow
    • the saved config round-trips
    • the Edit Colors note appears

Update:

  • bun test: 3283 pass, 0 fail (9375 expect() calls across 184 files).
  • Four new tests in colors-gradient.test.ts cover hex at 256 Color: the nearest-entry mapping for foreground and background (#FF8800 -> 208, #4e9a06 -> 64, #808080 -> 244), exact palette entries (#5f5f5f -> 59, #FF0000 -> 196), no truecolor escape through applyColors, and truecolor unchanged. The first three fail on the old code.
  • The same test file under Node (node node_modules/vitest/vitest.mjs run src/utils/__tests__/colors-gradient.test.ts): 18 of 18 pass.
  • bun run lint (type check and ESLint, no warnings allowed) is clean.
  • The nearest-color search was checked against a separate nearest-palette implementation over 636,056 colors (every third value per channel), with 0 mismatches.
  • Piped renders at 256 Color under a scratch HOME, in plain and Powerline mode: hex level colors #f5d020, #ff8800 and #e8343c come out as ESC[38;5;220m, ESC[38;5;208m and ESC[38;5;167m (before: ESC[38;2;…m). A separator colored hex:FF8800 comes out as ESC[38;5;208m.

Thinking Effort gains two options, both off by default. (b)rackets wraps
the rendered value in (), [], {} or <>. (l)evel colors opens an editor that
colors the value by effort level, with the brackets either matching the
level or keeping the widget color. With level colors on, the widget paints
only the level, and the renderer colors the rest as it colors any widget,
through a new optional Widget.colorsOnlyItsRuns().

At the 256 Color level, hex colors now render as the nearest 256-palette
color instead of a 24-bit code those terminals can't draw.
…olors test

The line and color editor tests no longer sleep a fixed 25ms after each
render and key. They wait for the key's visible effect, and the sleep
helper flushInk is gone. The color menu test that checks the note for a
widget that sets some of its own colors still called flushInk after the
first render, so it no longer compiled. It now waits until the frame
shows the Thinking Effort row, then reads that frame as before.
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