Skip to content

fix(web): picking an agent on /new no longer crashes the tree (CL-8523) - #916

Merged
TheGreatAxios merged 1 commit into
mainfrom
cl-8523-picker-crash
Sep 18, 2026
Merged

TheGreatAxios merged 1 commit into
mainfrom
cl-8523-picker-crash

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

What was wrong

FirstRunTour read its run flag once and never wrote it back:

const [run] = useState(() => !hasSeenTour(userId));

Closing, skipping or finishing the tour called markTourSeen(userId) — so it
would not replay on the next visit — but run stayed true, so the Joyride
instance stayed mounted and running for the whole session.

Verified against the running dev stack (fresh load of /new, tour's Close
clicked):

  • localStorage flag: set (…first-run-tour-seen:<user>=true)
  • document.body children: #root, #react-joyride-portal, #react-joyride-step-1
  • .react-joyride__beacon: present, .react-joyride__tooltip: present

So a "closed" tour is in fact an advanced-to-step-2 tour. Its overlay sits
above the page — browser automation refused clicks on the prompt textarea and
on + Add agent with "covered by div.react-joyride__overlay inside
div#react-joyride-portal"
— and with spotlightClicks (added in CL-8521)
Joyride also keeps a window mousemove listener that re-renders that
overlay portal on every mouse move.

Those two document.body containers are the only DOM in the app that is
created, appended and removed outside React's tree (JoyridePortal
appends/removes its own div, and react-floater's portal appends its
container from inside render()), and the containers are torn down and
recreated as the step index changes (#react-joyride-step-1
#react-joyride-step-3). A React commit that has to remove children from a
container that non-React code already detached is exactly what produces the
NotFoundError: Failed to execute 'removeChild' on 'Node' cascade up to
AppErrorBoundary, and it can only happen while Joyride is still mounted —
which, before this change, was always.

The fix

Dismissal now stops the tour as well as remembering it, so FirstRunTour
returns null and Joyride, its two body containers and its listeners are
unmounted the moment the person closes it. That also fixes the separate,
directly observed bug that the tour overlay keeps intercepting clicks on the
app after Close.

No new useEffect, no try/catch around React, no new tests (the change is
a one-line state flip; there is no red/green test that is not coverage
theater).

Not verified

The dev stack serves main's working tree, not this worktree, so the fix
itself was verified by reasoning plus tsc, lint, fmt, build and
bun test ./src (595 pass). I could not reproduce the removeChild
storm itself through headless automation: I created a Scribe agent and ran
the exact sequence (hard load of /new → tour Close → type a prompt →
+ Add agent → click Scribe) several times, including with a real hover,
with the step-2 tooltip open, after forcing the tour onto a step whose target
does not exist on /new, and with a flood of synthetic mousemove events
around the click — the console stayed clean every time. The post-Close state
above is reproduced reliably, and is the precondition for the reported crash;
whether some further real-mouse timing is also required could not be
established headlessly.

Closing, skipping or finishing the tour only persisted the seen flag;
the Joyride instance kept running for the rest of the session, holding
two containers appended to document.body and an overlay above the app.
Dismissal now also stops the tour so it unmounts.
@linear-code

linear-code Bot commented Sep 18, 2026

Copy link
Copy Markdown

CL-8523

@TheGreatAxios
TheGreatAxios merged commit 7257c68 into main Sep 18, 2026
4 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.

1 participant