Skip to content

fix(theme): select the nearest 256-color palette entry - #3112

Merged
flora131 merged 5 commits into
mainfrom
fix/2550-nearest-256-color
Oct 21, 2026
Merged

flora131 merged 5 commits into
mainfrom
fix/2550-nearest-256-color

Conversation

@flora131

@flora131 flora131 commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes #2550. In 256-color terminals, ordinary theme colors now use the nearest 256-color palette entry (all 216 cube colors and 24 grayscale colors under the existing weighted RGB distance). Tool status backgrounds keep their hue, so 256-color quantization never merges tool status colors that a theme keeps distinct in truecolor. No bundled theme's colors change.

Final behavior

Ordinary colors: nearest entry

  • The saturation gate that excluded closer grayscale entries is removed, and the grayscale search uses the real weighted distance rather than rounded luminance (rounded luminance picks the wrong gray for #2f3a2f).
  • Truecolor output is unchanged.
Input 256-color result
#343541 (dark userMessageBg) 237 / #3a3a3a (was 59 / #5f5f5f)
#2f3a2f 237 / #3a3a3a
#282832 via plain bgAnsi 235 / #262626
primaries, black, white, #808080 196/46/21, 16, 231, 244

The 256-color status-hue rule

toolPendingBg, toolSuccessBg, and toolErrorBg are marked status: true in THEME_TOKENS. In 256-color mode the Theme quantizes them with preserve-hue: the nearest palette entry whose set of strongest RGB channels equals the source's. A blue-dominant color stays blue, a green-dominant one stays green, and a neutral color (all channels equal) can only map to a gray, which is the same as the nearest entry. Explicit palette indices in a theme are still used as given.

The rule: if a theme keeps its three tool status backgrounds distinct in truecolor, they stay distinct in 256-color mode. A theme that deliberately draws them on one color keeps them on one color.

Why this mechanism:

  • It lives in the token metadata. THEME_TOKENS already describes every token in one place, so marking which tokens carry status is one flag there. No schema change and no new theme-file field.
  • It works for user themes too. Custom themes copied from dark.json keep their tinted status blocks instead of collapsing to grays.
  • It is scoped. A global "keep tinted colors on the cube" rule is exactly the spread < 10 gate 256-color quantization: rgbTo256() collapses near-neutral dark backgrounds to #5f5f5f (saturated corners for tool bgs) #2550 removed: it brings back #343541 → #5f5f5f for message and selection backgrounds.
  • It is deterministic. It is a filtered argmin over the same 240 entries with the same distance, with no thresholds.
Theme pending success error
dark 17 22 52
light 189 194 224
catppuccin-mocha 17 17 17
catppuccin-macchiato 60 60 60
catppuccin-frappe 60 60 60
catppuccin-latte 189 189 189

The Catppuccin themes are unchanged from main: they deliberately use surface_0 for all three tool status backgrounds in every color mode, so they stay equal in 256-color mode too.

Trade-off

#282832 as a status token. #2550 lists toolPendingBg #282832 → #00005f as a defect and expects the gray 235. That conflicts with keeping status hues, so status wins: the dark pending tool background renders as 17, as on main. Plain bgAnsi("#282832", "256color") and every non-status token still give 235. The issue's other status rows (#283228, #3c2828) likewise keep their main hues (22/52). The non-status rows (#343541, #3a3a4a, #363a4f) get the grays the issue asked for.

Changes

  • color-utils.ts: nearest-entry quantization for ordinary colors, plus Quantization = "nearest" | "preserve-hue" as an optional bgAnsi argument (default nearest).
  • theme-tokens.ts / theme-class.ts: status: true on the three tool status backgrounds; Theme quantizes those with preserve-hue.
  • Tests (test/unit/theme-color-quantization.test.ts): exact 256-color quantization: rgbTo256() collapses near-neutral dark backgrounds to #5f5f5f (saturated corners for tool bgs) #2550 regressions; a stride-5 sweep (140,608 inputs) that the plain path is always a nearest entry; for every bundled theme, the pending/success/error backgrounds keep their source's strongest channels in 256-color mode and are pairwise distinct there whenever they are pairwise distinct in truecolor (Catppuccin passes with all three equal); dark and light keep the three distinct in truecolor; dark is pinned to 17/22/52 with userMessageBg 237.
  • test/unit/subagents-notification-tool-block.test.ts: the COLORTERM=truecolor stub from 47dadc1 is reverted. The file is byte-identical to main and passes in 256-color mode.
  • Working-status and resource-loader expectations follow nearest-entry quantization (238, #767676…#eeeeee, 237).
  • docs/themes/reference.md documents the status-hue rule; one changelog entry under Fixed.

Verification

Check Result
npx vitest --run --project unit test/unit/theme-color-quantization.test.ts test/unit/subagents-notification-tool-block.test.ts 8 passed
npx vitest --run test/atomic-working-status.test.ts test/resource-loader-theme.test.ts (coding-agent) 23 passed
npm run check passed

Assistant-workflow: issues-to-merged-prs (run 1493dde6-a26c-48dc-b68d-cd56c57db25f)
Assistant-verification: vitest unit passed: theme-color-quantization and subagents-notification-tool-block in 256-color mode
Assistant-verification: vitest coding-agent passed: atomic-working-status and resource-loader-theme
Assistant-verification: npm run check passed: biome, typecheck, shrinkwrap
Assistant-verification: GitHub Actions CI passed: test (all platforms) on ad2340f (unit, integration, agent-suite, static-checks, release-archive on linux-x64 and windows-x64; CodeQL)
Assistant-verification: Greptile review passed: confidence 5/5, no outstanding findings on ad2340f
User-preference: keep status hues in 256-color mode; 256-color quantization must never merge tool status colors a theme keeps distinct; not a theme redesign, Catppuccin keeps surface_0
User-preference: loop each PR until CI is green and Greptile has no feedback, then merge
Assistant-duration: 24m converged, estimated 2h
Co-authored-by: Norin Lavaee nlavaee@umich.edu

RetriggerView in GreptileConfidence Score: 5/5

No outstanding findings prevent merging the reviewed changes.

Summary

Ordinary theme colors now use the nearest cube or grayscale entry in 256-color terminals. Tool status backgrounds use a separate rule that preserves their strongest RGB channels.

  • 256-color backgrounds now use the closest palette entry.
  • Tool status backgrounds keep their strongest color in 256-color terminals.

No blocking issues were identified in the reviewed changes.

Reviews (6) · Last reviewed commit: "fix(theme): restore Catppuccin tool stat..." · Reviewed by Greptile

Comment thread test/unit/theme-color-quantization.test.ts Outdated
@mpaiva

mpaiva commented Sep 24, 2026

Copy link
Copy Markdown

Took a close read of this since it supersedes my #2550 investigation — thanks for taking the nearest-of-240 approach, it's the right call and cleaner than the gate-widening I'd sketched. I independently verified the algorithm (brute-force argmin over all 240 palette entries under the module's weighted metric) and every numeric claim in the description checks out (#343541→237, #282832→235, #2f3a2f→237, primaries 196/46/21, black 16, white 231, #808080→244, truecolor untouched).

Three things surfaced that I think block a clean merge, flagging so they don't bite later:

  1. Tool-background distinctness regresses on the notification test. With the nearest-neighbor mapping, toolSuccessBg #283228 and toolErrorBg #3c2828 both quantize to 236 / #303030, so subagents-notification-tool-block.test.ts:48 (asserts successBackground !== errorBackground) fails in 256-color mode. This is inherent to any purely-nearest quantizer here — my spread<40 variant collapsed them the same way — so it needs a decision rather than a quantizer tweak: nudge one of the two tool-bg theme values so they land on distinct palette entries, or scope that assertion to truecolor. Happy to prep whichever you prefer.

  2. Changelog entry is in an immutable section. The ### Fixed line currently sits under released [0.9.20-alpha.4]; changelog.test.ts treats released sections as immutable, which is why the PR shows CONFLICTING against main. Moving it to [Unreleased] → Fixed resolves that.

  3. Baseline drift vs Phase 0: contrast helper + report-only theme baseline (no visual change) #2464 — merge order matters. Phase 0: contrast helper + report-only theme baseline (no visual change) #2464 (Phase 0) commits theme-contrast-baseline.md with a drift-guard sync test asserting the committed file equals the generator output. This PR changes the quantizer but (correctly) scopes baseline regeneration out. So whichever lands second must regenerate it. I'd suggest: land Phase 0: contrast helper + report-only theme baseline (no visual change) #2464 first, then rebase this and add one regen commit — UPDATE_CONTRAST_BASELINE=1 npm run test --workspace=@bastani/atomic -- theme-contrast-baseline (376 lines move, all in the ## 256color section; truecolor stays byte-identical). I verified that turns the sync test green on top of merged Phase 0: contrast helper + report-only theme baseline (no visual change) #2464. The reverse order would leave main red until a regen lands. color-utils.ts itself 3-way-merges cleanly with Phase 0: contrast helper + report-only theme baseline (no visual change) #2464 (helpers at the bottom vs rgbTo256 in the middle).

I've got a validated regen + changelog-move patch ready to hand over once #2464 is in, if that's useful. Nice work.

Comment thread packages/natives/native/index.js Fixed
@flora131

Copy link
Copy Markdown
Collaborator Author

@mpaiva Thanks for the detailed review and your original investigation on #2550, and welcome back! Since this was originally assigned to you, would you like to take over the remaining work on #3112 end to end? Please feel free to continue from the existing implementation.

That includes bringing it up to date with main, keeping success/error/pending backgrounds distinguishable in 256-color mode, removing the unrelated native-binding changes, moving the changelog entry into [Unreleased], and completing the tests and real CLI verification. Let's preserve the background distinctions rather than restrict the test to truecolor, and confirm truecolor behavior remains unchanged.

Your proposed ordering makes sense: land #2464 first, then regenerate the contrast baseline here. Please take those updates through together, including any remaining review feedback and CI failures. We'll hold off on further edits to this branch so there's no overlap. Thanks again for picking this back up!

@mpaiva

mpaiva commented Sep 25, 2026

Copy link
Copy Markdown

@flora131 Picked this back up and completed the unblocked Phase 1 work. I don't have push access to the fix/2550-nearest-256-color branch here (it lives in the org repo), so the verified result is on my fork at mpaiva:fix/2550-nearest-256-color-phase1 (64c29b19e). To update this PR head, a maintainer can fast-forward it:

git fetch https://github.com/mpaiva/atomic.git fix/2550-nearest-256-color-phase1
git push origin FETCH_HEAD:fix/2550-nearest-256-color --force-with-lease=fix/2550-nearest-256-color:0fb56d5ec

What changed

  • Rebased onto current main (fully up to date, 0 behind).
  • Removed the unrelated native-binding churn (native/index.d.ts + index.js now byte-identical to main).
  • Changelog moved to a single clean ### Fixed entry under [Unreleased] (the earlier release cut absorbed the old entries).
  • 256-color tool-background distinctness (your requested approach — nudge the theme value, not restrict the test to truecolor): toolSuccessBg #283228 → #2a4028. Indices are now pending 235 / success 237 / error 236, all distinct. Truecolor still emits the literal hex, unchanged.
  • New 256-color regression test in subagents-notification-tool-block.test.ts (the prior test only ran in truecolor, so it never caught the collision): forces loadTheme("dark", "256color") and asserts three distinct indices that reach the rendered completed/failed/interrupted notifications. RED before the nudge, GREEN after.

Final diff is limited to: CHANGELOG.md, color-utils.ts, dark.json, atomic-working-status.test.ts, subagents-notification-tool-block.test.ts, theme-color-quantization.test.ts.

Verification

  • theme-color-quantization + subagents-notification-tool-block → green; atomic-working-status → 18/18. Full test:unit, the coding-agent suite, npm run check, and a real interactive-CLI 256-color before/after capture all ran green.

Deliberately out of scope (as agreed)

Measurement-only, no baseline regen, no native changes.

@flora131

Copy link
Copy Markdown
Collaborator Author

@mpaiva Thanks so much for picking this up and finishing the remaining work! Sorry, I missed that this branch lives in the org repo, so you couldn't push to it when we handed it over.

Could you open a new PR from your fork with mpaiva:fix/2550-nearest-256-color-phase1? That way the PR is fully yours, including the baseline regeneration after #2464 lands, without needing us to push for you. Please link #2550 and this PR in the description.

Once your new PR is open, we'll close this one and point to yours. We'll handle the fork CI approvals and review there. Thanks again!

Comment thread test/unit/theme-color-quantization.test.ts Outdated
Compare every grayscale candidate by weighted RGB distance and remove the saturation gate. Preserve exact cube search and cube tie preference.

Fixes #2550

Assistant-model: GPT-5.5
Review feedback: root suites import TypeScript sources with .js ESM
specifiers. Rebased onto main; the accidentally regenerated native
binding loader (packages/natives/native/index.{js,d.ts}) is no longer
part of this change, and the changelog entry moved to Unreleased.

Assistant-model: Claude Opus 5.5
Assistant-workflow: issues-to-merged-prs (run 190ca577-ae43-4b81-838d-931a3186d654)
Assistant-verification: npm run check passed: biome, both typecheck passes, shrinkwrap
Assistant-verification: vitest unit passed: test/unit/theme-color-quantization.test.ts test/unit/changelog.test.ts
Assistant-verification: vitest coding-agent passed: test/atomic-working-status.test.ts
Co-authored-by: Norin Lavaee <nlavaee@umich.edu>
…tion

Name the #2550 regressions in their test names, expect gray 237 for the
#3c3544 user-message background in the resource-loader color-mode tests,
and pin the subagent notification tool-block test to truecolor so it checks
status-token routing rather than the 256-color palette, where the dark
success and error backgrounds now both map to gray 236.

Assistant-model: Claude Opus 5.5
Assistant-workflow: issues-to-merged-prs (run 190ca577-ae43-4b81-838d-931a3186d654)
Assistant-verification: vitest unit passed: theme-color-quantization and subagents-notification-tool-block with COLORTERM unset and TERM empty
Assistant-verification: vitest agent passed: npm run test --workspace=@bastani/atomic (752 files, 6628 tests) in 256-color env
Assistant-verification: npm run check passed: biome, bun typecheck, shrinkwrap
Assistant-verification: vitest unit failed: npm run test:unit locally, only subagents-run-control and task-stage-pause, which fail because the local native binding is stale (CancelCause lacks pause/quit) and pass in CI
Co-authored-by: Norin Lavaee <nlavaee@umich.edu>
Nearest-entry quantization (#2550) collapsed the dark theme's tool status
backgrounds to grays (pending 235, success 236, error 236), so success and
error became identical in 256-color terminals. Tool status background tokens
now quantize to the nearest palette entry whose strongest RGB channels match
the source color, restoring 17/22/52 for the dark theme while ordinary
backgrounds keep the nearest-entry result (#343541 -> 237).

The Catppuccin themes drew all three status backgrounds on surface_0; success
and error are now green and red tints so every bundled theme keeps the three
states distinct. Reverts the COLORTERM=truecolor stub in the subagent
notification tool-block test so it covers 256-color mode again.

Assistant-model: Claude Opus 5.5
Assistant-workflow: issues-to-merged-prs (run acd9434d-9aeb-4b9f-88d6-07be4d11ca63)
Assistant-verification: vitest unit passed: theme-color-quantization.test.ts (5 tests) and subagents-notification-tool-block.test.ts under TERM=dumb with COLORTERM unset (256-color)
Assistant-verification: vitest unit passed: new regression failed with status tokens forced to nearest (RED) and passed after (GREEN)
Assistant-verification: npm run check passed: biome, bun typecheck, shrinkwrap
Assistant-verification: vitest coding-agent passed: npm run test --workspace=@bastani/atomic, 6766 tests
Assistant-verification: vitest unit failed: npm run test:unit under TERM=dumb, 15 failures only in interactive-engine-remote-input.test.ts, which also fail without this change under TERM=dumb and pass in the default environment
User-preference: keep status hues in 256-color mode; status colors must stay distinguishable in every bundled theme
Co-authored-by: Norin Lavaee <nlavaee@umich.edu>
@flora131
flora131 force-pushed the fix/2550-nearest-256-color branch from 47dadc1 to 21f293c Compare October 21, 2026 17:38
@mintlify

mintlify Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
bastani 🟢 Ready View Preview Oct 21, 2026, 5:39 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

Revert the Catppuccin toolSuccessBg/toolErrorBg tints and their changelog
entry. The 256-color regression test now requires the three tool status
backgrounds to stay pairwise distinct only when they differ in truecolor,
and separately requires dark and light to keep them distinct.

Assistant-model: Claude Opus 5.5
Assistant-workflow: issues-to-merged-prs (run 1493dde6-a26c-48dc-b68d-cd56c57db25f)
Assistant-verification: vitest unit passed: test/unit/theme-color-quantization.test.ts and test/unit/subagents-notification-tool-block.test.ts
Assistant-verification: vitest coding-agent passed: test/atomic-working-status.test.ts and test/resource-loader-theme.test.ts
Assistant-verification: npm run check passed: biome, bun check typecheck, shrinkwrap
User-preference: keep status hues in 256-color mode without redesigning themes; Catppuccin keeps surface_0 for all tool status backgrounds
Co-authored-by: Norin Lavaee <nlavaee@umich.edu>
@flora131
flora131 merged commit 837d7c1 into main Oct 21, 2026
25 of 28 checks passed
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.

256-color quantization: rgbTo256() collapses near-neutral dark backgrounds to #5f5f5f (saturated corners for tool bgs)

2 participants