feat: custom accent and user-message-box colors #49

Merged
owlburtoe merged 2 commits from clfy-21-custom-colors into master 2026-07-16 21:00:15 -04:00
Owner

Closes CLFY-21

accentColor (was claude|theme) and userMessageBox (was theme|claude|off) now
also accept a user-supplied hex. Both reuse the existing footerColor text-row machinery —
same normalizeHexColor validation, same inline text sub-mode — rather than adding a second
hex input path. The keywords keep working exactly as before; a hex is simply another accepted
value.

Storing escapes, never hex

pi's Theme stores ready-made ANSI escapes in fgColors/bgColorsfg() only
concatenates. Storing hex renders a literal #B1B9F9, widening the line past the terminal,
and pi's renderer throws on overflow. That is the 2.3.0 crash, so this is the invariant the
feature had to respect.

Rather than hand-rolling a second quantizer, ansiFromHex constructs a throwaway pi Theme
and reads getFgAnsi/getBgAnsi back out, delegating conversion to pi's own fgAnsi/
bgAnsi. Verified against pi's compiled source (not its .d.ts alone): the constructor
iterates only the keys you pass, so the partial color records are safe, and both lookups use
fixed keys that are always present. Tests assert the stored value is an escape and never hex,
in both truecolor and 256-color mode.

Review gate found a real bug (fixed in c78b609)

Setting a custom accent and switching back to Accent: theme never restored pi's own accent —
the custom color stranded itself until pi restarted. Reproduced before fixing:

after custom (proxy) : ESC[38;2;18;52;86m
after accent=theme   : ESC[38;2;18;52;86m   <- should be pi's teal

pi hands the same logical theme over as both the instance and a forwarding Proxy, and the
snapshot WeakMap was keyed on object identity — so whichever identity arrived second saw the
override already installed and never recorded the theme's real accent. CC_ACCENT_VALUES
existed to prevent exactly this, but listed only the two captured lavenders, so a custom hex
fell straight through.

Note this was latent for the claude accent too: the same restore path was already broken
before this ticket. The custom-color work only made it reachable.

Fix: key the snapshot on the fgColors container (which forwards through the Proxy, so it
identifies the logical theme), and recognize every escape claudify imposes rather than a
fixed list. A Proxy regression test now covers it.

Review

An adversarial reviewer independently identified the same Proxy/snapshot corruption from the
pre-fix code, and independently verified the PiTheme constructor contract against pi's
source. It found no other high-confidence issues, and traced two open concerns to ground:
writeSettingsKey clears the settings cache before onSettingChange, so the
userBoxCustomBg staleness window isn't reachable; and a custom hex never reaches a
theme.fg() key argument, since these are text-kind rows rendered via a fixed valid key.
Caveat: the gate fix landed mid-review, so it re-read against final state and asked for a
re-verify — which is what the validation below is.

Validation

  • bun run test15/15 green at c78b609, including the new Proxy regression test.
  • npx tsc --noEmit --strict … (the four extensions) — clean.
  • The worker's own pre-push run failed test-diff-body twice; that is the known shiki flake
    on a fresh per-worktree npm install, and it settles green on re-run. The worker correctly
    refused to bypass its hook and stopped to report rather than papering over it.

Not verified live. Per CLAUDE.md, mutating pi structures wants a live smoke test before
release — this changes theme color storage, the exact surface that shipped broken in 2.3.0.
Worth eyeballing a custom accent, and a switch back to theme, in a real pi session.

Deferred (not in this PR)

  • On a pi theme switch, applyThemePaletteIfNeeded refreshes the accent but not the
    user-box background, so a custom box color keeps the previous theme's color-mode escape. Low
    severity (a truecolor escape still renders in practice), out of scope here.
  • CC's exact inline-code color remains uncaptured; mdCode stays accent-aliased, as the repo
    documents. Deliberately not guessed.
Closes CLFY-21 `accentColor` (was `claude`|`theme`) and `userMessageBox` (was `theme`|`claude`|`off`) now also accept a user-supplied hex. Both reuse the existing `footerColor` text-row machinery — same `normalizeHexColor` validation, same inline text sub-mode — rather than adding a second hex input path. The keywords keep working exactly as before; a hex is simply another accepted value. ## Storing escapes, never hex pi's `Theme` stores ready-made ANSI escapes in `fgColors`/`bgColors` — `fg()` only concatenates. Storing hex renders a literal `#B1B9F9`, widening the line past the terminal, and pi's renderer throws on overflow. That is the 2.3.0 crash, so this is the invariant the feature had to respect. Rather than hand-rolling a second quantizer, `ansiFromHex` constructs a throwaway pi `Theme` and reads `getFgAnsi`/`getBgAnsi` back out, delegating conversion to pi's own `fgAnsi`/ `bgAnsi`. Verified against pi's compiled source (not its `.d.ts` alone): the constructor iterates only the keys you pass, so the partial color records are safe, and both lookups use fixed keys that are always present. Tests assert the stored value is an escape and never hex, in **both** truecolor and 256-color mode. ## Review gate found a real bug (fixed in c78b609) Setting a custom accent and switching back to `Accent: theme` never restored pi's own accent — the custom color stranded itself until pi restarted. Reproduced before fixing: ``` after custom (proxy) : ESC[38;2;18;52;86m after accent=theme : ESC[38;2;18;52;86m <- should be pi's teal ``` pi hands the same logical theme over as **both the instance and a forwarding Proxy**, and the snapshot `WeakMap` was keyed on object identity — so whichever identity arrived second saw the override already installed and never recorded the theme's real accent. `CC_ACCENT_VALUES` existed to prevent exactly this, but listed only the two captured lavenders, so a custom hex fell straight through. Note this was **latent for the `claude` accent too**: the same restore path was already broken before this ticket. The custom-color work only made it reachable. Fix: key the snapshot on the `fgColors` container (which forwards through the Proxy, so it identifies the *logical* theme), and recognize every escape claudify imposes rather than a fixed list. A Proxy regression test now covers it. ## Review An adversarial reviewer independently identified the same Proxy/snapshot corruption from the pre-fix code, and independently verified the `PiTheme` constructor contract against pi's source. It found no other high-confidence issues, and traced two open concerns to ground: `writeSettingsKey` clears the settings cache *before* `onSettingChange`, so the `userBoxCustomBg` staleness window isn't reachable; and a custom hex never reaches a `theme.fg()` key argument, since these are `text`-kind rows rendered via a fixed valid key. Caveat: the gate fix landed mid-review, so it re-read against final state and asked for a re-verify — which is what the validation below is. ## Validation - `bun run test` — **15/15 green** at `c78b609`, including the new Proxy regression test. - `npx tsc --noEmit --strict …` (the four extensions) — **clean**. - The worker's own pre-push run failed `test-diff-body` twice; that is the known `shiki` flake on a fresh per-worktree `npm install`, and it settles green on re-run. The worker correctly refused to bypass its hook and stopped to report rather than papering over it. **Not verified live.** Per CLAUDE.md, mutating pi structures wants a live smoke test before release — this changes theme color storage, the exact surface that shipped broken in 2.3.0. Worth eyeballing a custom accent, and a switch back to `theme`, in a real pi session. ## Deferred (not in this PR) - On a pi **theme switch**, `applyThemePaletteIfNeeded` refreshes the accent but not the user-box background, so a custom box color keeps the previous theme's color-mode escape. Low severity (a truecolor escape still renders in practice), out of scope here. - CC's exact inline-code color remains uncaptured; `mdCode` stays accent-aliased, as the repo documents. Deliberately not guessed.
fix: share the accent snapshot across pi's theme instance and its proxy
All checks were successful
Plane Sync / Close referenced Plane work items (pull_request) Successful in 2s
c78b609494
Keying the snapshot on the theme object gave the instance and pi's
forwarding Proxy separate entries, so whichever arrived second saw the
override already installed and never recorded the theme's real accent.
accentColor=theme then restored the override instead of pi's own color,
stranding a custom accent until restart. Key on the fgColors container,
which forwards through the Proxy, and recognize every escape we impose.
owlburtoe merged commit af8eeaf2dd into master 2026-07-16 21:00:15 -04:00
owlburtoe deleted branch clfy-21-custom-colors 2026-07-16 21:00:15 -04:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
owlburtoe/pi-claudify!49
No description provided.