fix(pi): rescue undecorated composer + restore removed call button (#460)#467
fix(pi): rescue undecorated composer + restore removed call button (#460)#467rosscado wants to merge 1 commit into
Conversation
) On the first pi.ai/talk load of a session the call button could be missing until a reload. Investigation (live cold-cache probes + code tracing) found the prompt was the only decoration target with NO document-wide rescue path: - The content-loaded handler re-scans document.body for audio controls, the sidebar, and chat history — but never the prompt. A composer that mounted while route observation was stopped (the 300ms route poll stops the MutationObserver on transiently non-chatable paths during Pi's boot) stayed bare forever. - bootstrapInitialDecoration gave up after ~7.5s, while a live cold-cache probe measured Pi's composer mounting at t≈25s; it also self-terminated permanently if one retry tick landed on a non-chatable path. - A host re-render that destroys the injected button while reusing the same textarea (which keeps id=saypi-prompt) left every finder reporting "already decorated", so the button was never re-created. Fixes: - content-loaded now rescues the prompt (findAndDecoratePrompt on body) and restores a missing call button. - bootstrapInitialDecoration retries for ~30s and survives transiently non-chatable ticks. - The mutation removal branch detects a removed #saypi-callButton and re-creates it in place (decoratePromptControls extracted for reuse, guarded against duplicate buttons and stacked submit-button observers). - updateButtonSegments no longer console.errors during the (re)creation gap before the async SVG mounts — it's a normal transient now. Fail-first specs cover all four paths (PiBootstrapInitialLoad.spec.ts, CallButton-svg-mount-gap.spec.ts). Layer 3 e2e green; Layer 4 (CDP) verified on live pi.ai/talk (decoration + full synthetic voice turn). Closes #460 Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VVm2qYNQoMSQMUpnWcriJE
Reviewer verdict (independent, post-hoc gate completion): APPROVEIndependent adversarial review completing the merge gate for a sibling session's finished work. I traced the bootstrap machinery end-to-end on main vs. the branch, ran the PR's specs in its worktree, and empirically re-verified the fail-first claim against main. 1. The three miss vectors each have a real recovery path
2. Regression hunt — clean
3. Tests — fail-first empirically confirmed
4. AGENTS.md disciplineMinimal diff (2 source + 2 test files, no Non-blocking observations
Solid, well-evidenced fix. Approving. |
|
Merge gate complete — requesting founder merge. CI green + independent reviewer APPROVE (verdict above, fail-first re-proved in both directions). Two agent sessions were classifier-blocked from merging (auto-merge grant covers only self-opened PRs), so this one's yours, @rosscado. Merging completes #460 (closed as deferred-on-merge). |
|
Not approved - changes requested.
Reproduced on builds produced with |

Problem
On the initial load of https://pi.ai/talk the SayPi call button could be missing from the composer until a page reload or an SPA navigation away and back (#460). Everything else (sidebar buttons, audio controls) decorated normally.
Root cause
The prompt was the only decoration target with no document-wide rescue path, so any missed mount was permanent:
content-loadednever decorated the prompt. The handler re-scansdocument.bodyfor audio controls, the sidebar, and chat history — but not the composer. If the composer mounted while route observation was stopped (the 300ms route poll callsstopObservingDom()on transiently non-chatable paths during Pi's boot), nothing ever decorated it — matching the report exactly: no call button, everything else normal, reload fixes it.<textarea>(which keepsid=saypi-prompt) leaves every finder reporting "already decorated" — the button could never come back.Investigation details (instrumented headed-CDP probes against live pi.ai, mutation/URL timelines) are on the issue.
Fix
content-loadednow rescues the prompt (findAndDecoratePrompt(document.body)) and restores a missing call button (ensureCallButtonPresent).bootstrapInitialDecorationretries for ~30s and survives transiently non-chatable ticks.#saypi-callButtonand re-creates it in place.decoratePromptControlsextracted fromdecoratePromptfor reuse, guarded against duplicate buttons and stacked submit-button observers.updateButtonSegmentsno longerconsole.errors in the (re)creation gap before the async SVG mounts — with the restore path this is a normal transient (it surfaced as flaky page errors in the Layer 3 suite until downgraded).Testing
test/chatbots/PiBootstrapInitialLoad.spec.ts(route-blip miss, content-loaded rescue, ~20s late mount, button-removal restore) andtest/buttons/CallButton-svg-mount-gap.spec.ts.Closes #460
🤖 Generated with Claude Code
https://claude.ai/code/session_01VVm2qYNQoMSQMUpnWcriJE