Skip to content

fix(pi): add the "More voices" door to Pi's Voice settings page (#491 follow-up)#498

Merged
rosscado merged 1 commit into
mainfrom
fix/pi-settings-more-voices-door
Jul 4, 2026
Merged

fix(pi): add the "More voices" door to Pi's Voice settings page (#491 follow-up)#498
rosscado merged 1 commit into
mainfrom
fix/pi-settings-more-voices-door

Conversation

@rosscado

@rosscado rosscado commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Why

The founder's door-first fix (#496) restored the "More voices" door in Pi's in-chat menu. The same door was missing from Pi's own Voice settings page (pi.ai/profile/settings) — the founder asked to add it there too. Two stale-DOM causes, both verified live (Layer-4 CDP, headed real Chrome, 2026-07-04):

  1. getVoiceSettingsSelector matched 0. Pi redesigned the page: the content column drifted mx-auto w-full px-6 py-10mx-auto w-full max-w-2xl flex-1 px-6 py-3 pt-20, and the card grid grid grid-cols-2 gap-4grid grid-cols-1 gap-4 sm:grid-cols-2. Re-anchored to the settings content column (max-w-2xl scopes it off the wider /discover /threads surfaces) + its direct-child responsive card grid.
  2. Even with a matching selector, decoration never fired. findAndDecorateVoiceSettings was gated behind sidebar-decoration in the same added node and searched that node — but the grid lives in the main content, and the settings route has no chat prompt, so the content-loaded chain that scans for voice settings never runs there. Un-gated it in the bootstrap to scan document.body once per mutation batch (idempotent via the getElementById guard; a no-op on ChatGPT — empty selector — and Claude — its stale Pi selector matches 0 there).

What changed (door-first)

PiVoiceSettings now injects only the "More voices" door (deferring inline SayPi voice rows — companion to the in-chat #497), cloning a native card's class list so it renders Pi-native (settings cards are <button>s here). A shared buildMoreVoicesDoor(template, tag) helper backs both Pi doors (in-chat <div>, settings <button>). The door requires a real template and waits for Pi's cards via observeSettingsGrid's MutationObserver, so it can never render unstyled and re-injects if Pi's React drops it.

Adversarial review

Ran a 4-lens review workflow on the diff (it touches the shared bootstrap). All four confirmed findings are fixed:

  • empty-grid race (unstyled door if the grid decorates before Pi's cards) → template guard + test.
  • selector over-match on /discover /threads → tightened with max-w-2xl, live-verified 0 matches on all three non-settings surfaces.
  • missing selector-contract test (reverting the selector kept CI green) → Pi-VoiceSettingsSelector.spec.ts (old literal matches 0, new matches 1, no match on talk/wider grids).
  • untested observer re-injection → a test driving the real MutationObserver.

Tests & evidence

  • Full gate green: npm test (tsc + Jest + Vitest, 203 files / 1786 tests) ✅.
  • Live on pi.ai/profile/settings: door decorates, renders as the last grid card, native-styled, 0 console errors.
  • Live selector sweep (tightened): /profile/settings → 1 match + door; /discover /threads /talk0 matches, no stray door.
  • fix(pi): restore the "More voices" door in Pi's redesigned in-chat menu (#491) #496 in-chat door unaffected (re-verified live: door still injects as the last menu row).

Refs #491, #496, #497.

🤖 Generated with Claude Code

https://claude.ai/code/session_012WmjGuAUmFGQBZukitHp4L

…follow-up)

The founder's #491 door-first fix (#496) restored the door in Pi's in-chat
menu; the same door was missing from Pi's own Voice settings page
(pi.ai/profile/settings). Two stale-DOM causes, both verified live (Layer-4 CDP,
2026-07-04):

1. getVoiceSettingsSelector matched 0 — Pi redesigned the page: the content
   column drifted `px-6 py-10` → `mx-auto w-full max-w-2xl px-6 py-3 pt-20` and
   the card grid `grid-cols-2` → `grid-cols-1 gap-4 sm:grid-cols-2`. Re-anchored
   to the settings content column (`max-w-2xl` keeps it off the wider /discover
   /threads surfaces) + its direct-child responsive card grid.
2. Even with a matching selector, decoration never fired: findAndDecorateVoiceSettings
   was gated behind sidebar-decoration in the same added node AND searched that
   node — but the grid is in the main content, and the settings route has no chat
   prompt, so the content-loaded chain never runs there. Un-gated it in the
   bootstrap to scan document.body once per mutation batch (idempotent via the
   getElementById guard; a no-op on ChatGPT (empty selector) and Claude (its
   stale Pi selector matches 0 there)).

Door-first: PiVoiceSettings now injects only the "More voices" door (deferring
inline SayPi voice rows, companion to the in-chat #497), cloning a native card's
class list so it renders Pi-native (cards are `<button>`s here). A shared
buildMoreVoicesDoor(template, tag) helper backs both Pi doors (in-chat `<div>`,
settings `<button>`); the door requires a real template (waits for Pi's cards via
observeSettingsGrid's MutationObserver) so it can never render unstyled.

Adversarial-reviewed (4-lens workflow); the four confirmed findings are fixed:
empty-grid race (template guard), selector over-match (tightened + live-verified
0 on /discover /threads /talk), missing selector-contract test
(Pi-VoiceSettingsSelector.spec.ts), untested observer re-injection (real
MutationObserver test). Full gate green (203 files / 1786 tests). Verified live:
door renders as the last grid card, native-styled, 0 console errors; no spurious
decoration on /discover /threads /talk; #496 in-chat door unaffected.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_012WmjGuAUmFGQBZukitHp4L
@rosscado rosscado merged commit cfcda66 into main Jul 4, 2026
2 checks passed
@rosscado rosscado deleted the fix/pi-settings-more-voices-door branch July 4, 2026 22:47
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.

1 participant