Skip to content

ChatGPT: call button's onclick (and our body class / build stamp) are dropped after hydration #595

Description

@rosscado

What I measured

On a live https://chatgpt.com/ load, roughly 2 seconds after #saypi-callButton appears:

#saypi-callButton                      → present, exactly 1
#saypi-callButton.onclick              → null        ← the click handler property is gone
document.documentElement.dataset.saypiBuild → null   ← our build stamp attribute is gone
document.body.className                → "unlocked"  ← the "chatgpt" flag class is gone

For contrast, the same probe on pi.ai returns a build stamp, body class="pi unlocked", and a live onclick.

updateCallButton sets callButton.onclick = onClick (src/buttons/CallButton.ts), and ChatbotService.addChatbotFlags() adds the chatgpt class to <body>, and stampBuildOnDocument() sets <html data-saypi-build>. All three are gone a couple of seconds later — attributes and JS properties — which points at ChatGPT's React/Next hydration replacing or resetting those nodes rather than at three separate bugs. A React hydration error (Minified React error #418) is logged on the same page.

Clicking the button immediately after it appears does work (measured: label → "End voice mode", callStateactive), which is presumably why the host sweep passes — it drives its turn promptly.

Why it matters

If the button's onclick is genuinely dropped once hydration settles, a ChatGPT user who lands on the page and clicks a few seconds later gets a button that looks right and does nothing. That would be worse than a missing button, because there is no visible signal anything is wrong.

Two things are unproven and should be established first:

  1. Is the button actually dead after hydration, or only the onclick property while a listener elsewhere still works? Click it ~5s and ~30s after load and observe callState.
  2. Does CallButton.this.element still point at the node in the DOM? If React cloned the node, SayPi holds a detached element and every subsequent state update (icon, segments, glow, enable/disable) drives a node nobody can see.

Relationship to #593

Distinct failure mode. #593 (fixed in #594) is element absent, which ensureCallButtonPresent() detects and repairs. This one is element present but inert — a presence check can't see it, so #594 does not address it and shouldn't be assumed to.

If confirmed, the likely remedy is to stop relying on JS properties surviving on host-owned subtrees: bind with addEventListener, verify CallButton.this.element.isConnected on each state update, and re-adopt (or re-create) the button when the node in the DOM is not the one we hold.

Not a regression

Measured identically on main (a52dd18) and on the #594 branch, so this predates both. Found while doing cross-host verification for #593.

Repro: Layer 4 (CDP), real Chrome, dev build loaded, https://chatgpt.com/, wait for #saypi-callButton, then after ~2s evaluate document.getElementById("saypi-callButton").onclick.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions