Skip to content

Stop translated portals taking the whole app down - #4704

Open
nachocossio wants to merge 1 commit into
mainfrom
fix/translated-portal-teardown-crash
Open

nachocossio wants to merge 1 commit into
mainfrom
fix/translated-portal-teardown-crash

Conversation

@nachocossio

@nachocossio nachocossio commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator

What broke

A browser translator rewrites text nodes in place. React then holds references to nodes that are no longer children of their parent, and tearing down a portaled surface throws NotFoundError. React Router catches it at the global errorElement (router.tsx:508-515), so the user loses the whole app to the "Something went wrong" screen — mid-chat, on the Playground.

Evidence

INSPECTOR-CLIENT-27X is the alerted case: Safari 26.6.2 on an English UI, NotFoundError: The object can not be found here. (DOMException code 8), release: 3.3.5.

Breadcrumbs from that session:

14:44:04  3 x POST /api/web/chat-v2      chat streaming
14:44:34  click
14:44:34  Warning: Missing `Description` ... for {DialogContent}    a Radix dialog opened
14:44:36  clicks + typing in two inputs
14:44:41  NotFoundError -> React Router caught it

The stack is entirely React internals, ending in commitDeletionEffectsOnFiber → recursivelyTraverseDeletionEffects → native removeChild, with the frame that enters case 4 / containerInfo. Fiber tag 4 is HostPortal: React was deleting a subtree containing a portal and the node was gone from the container.

54 prod events over 30 days, ~17 users, releases 2.35.1 → 3.3.6 — chronic, not a regression. Browsers: Chrome, Chrome Mobile, Chrome Mobile iOS, Safari, Mobile Safari, Edge. Not one Firefox, the only browser of those without translation offered by default.

f4009ba17 ("Prevent translated server dialog teardown crash", #4056) already diagnosed this in August and named the mechanism in a code comment — but fixed exactly one dialog.

The change

Move the opt-out to the shared primitives, so all 89 DialogContent call sites and every other portaled surface are covered in one place: dialog, alert-dialog, sheet, popover, dropdown-menu, select, tooltip, hover-card, context-menu, menubar — plus the three sub-menu contents, which portal separately and so do not inherit the parent surface's opt-out.

ServerDetailModal's local copy goes with it: the primitive supplies it now, and keeping both emitted notranslate twice in the DOM. Its August test still passes, and now proves the primitive covers a component that passes its own className.

What this does not fix

The insertBefore variant (27Y, 289, 25Y), where React places a conditional child (Toaster, MCPJamLimitDialog, PlanLimitDialog, SessionRefreshBanner) into the #root container. Closing that needs <html translate="no">, which turns off translation for the whole app — a product call, so it is deliberately not in this PR.

Also out of scope, tracked separately: sonner's Toaster (accepts classes only, and a bare notranslate does not protect Safari), and three structural defects that throw the same exception with no translator involved — widget-surface-host reparenting a portal container outside React, HostsTab's AnimatePresence mode="sync" wrapping both a portal container and its portal source, and key={i} over the streaming message-parts list.

Testing

13 new tests in design-system-portal-translate.test.tsx, written first and watched fail for the right reason (element renders, attribute is null) before the fix.

design-system typecheck    clean
typecheck:client           clean (includes check:renderer-tier-b)
client suite               3 failed / 1093 passed files, 12306 tests passed

Those 3 failures are pre-existing: score-run-resume, scenario-chat-transcript and eval-live-chat-panel fail identically on a stashed clean tree. All storage/filesystem tests, unrelated to this diff.


Summary by cubic

Stops browser translators from crashing the app when portaled surfaces unmount by moving the translate="no" opt-out to every portaled primitive in @mcpjam/design-system.

  • Covers all dialog, alert-dialog, sheet, popover, dropdown-menu, select, tooltip, hover-card, context-menu, and menubar surfaces, including sub-menus.
  • Removes the one-off opt-out from ServerDetailModal and its duplicate notranslate class.
  • Adds tests verifying every surface renders with both translate="no" and notranslate.

Written for commit 45e3be1. Summary will update on new commits.

Review in cubic

A browser translator rewrites text nodes in place. React then holds
references to nodes that are no longer children of their parent, and when it
tears down a portaled surface the removal throws NotFoundError. React Router
catches it at the global errorElement, so the user loses the entire app to
the "Something went wrong" screen — mid-chat, on the Playground.

Sentry INSPECTOR-CLIENT-27X is the alerted case: Safari on an English UI,
"The object can not be found here." (DOMException code 8), fired 7s after a
Radix dialog opened, with a stack ending in commitDeletionEffectsOnFiber ->
removeChild against a HostPortal container. 54 events over 30 days across
Chrome, Safari and Edge; not one from Firefox, the only browser of those
without translation offered by default.

f4009ba diagnosed this in August and fixed one dialog. Move the opt-out to
the shared primitives so all 89 DialogContent call sites and every other
portaled surface are covered: dialog, alert-dialog, sheet, popover,
dropdown-menu, select, tooltip, hover-card, context-menu and menubar, plus
the three sub-menu contents, which portal separately and so do not inherit
the parent surface's opt-out.

ServerDetailModal's local copy goes with it — the primitive now supplies it,
and keeping both emitted `notranslate` twice. Its August test still passes,
and now proves the primitive covers a component that passes its own
className.

This does not cover the insertBefore variant (INSPECTOR-CLIENT-27Y, 289,
25Y), where React places a conditional child into the #root container.
Closing that needs a global `<html translate="no">`, which turns off
translation for the whole app and is a product call.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@chelojimenez

Copy link
Copy Markdown
Contributor

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Internal preview

Preview URL: https://mcp-inspector-pr-4704.up.railway.app
Deployed commit: 66c6068
PR head commit: 45e3be1
Backend target: staging fallback.
Health: ✅ Convex reachable
Access is employee-only in non-production environments.

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: c414f1a9-3e6a-427a-8995-cb1cc6973995

📥 Commits

Reviewing files that changed from the base of the PR and between 2b4e784 and 45e3be1.

📒 Files selected for processing (12)
  • design-system/src/components/alert-dialog.tsx
  • design-system/src/components/context-menu.tsx
  • design-system/src/components/dialog.tsx
  • design-system/src/components/dropdown-menu.tsx
  • design-system/src/components/hover-card.tsx
  • design-system/src/components/menubar.tsx
  • design-system/src/components/popover.tsx
  • design-system/src/components/select.tsx
  • design-system/src/components/sheet.tsx
  • design-system/src/components/tooltip.tsx
  • mcpjam-inspector/client/src/components/__tests__/design-system-portal-translate.test.tsx
  • mcpjam-inspector/client/src/components/connection/ServerDetailModal.tsx

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


Walkthrough

Design-system portal surfaces now set translate="no" and the notranslate class. Coverage includes dialogs, menus, selects, tooltips, hover cards, popovers, sheets, and nested submenu surfaces. The server detail modal removes its duplicate translation workaround. Vitest tests verify the attributes on rendered top-level and nested surfaces.

Merge Risk: ⚪ Minimal · up to 45e3b

Portal-based design-system surfaces now opt out of browser translation, including nested menus, while preserving the ServerDetailModal behavior through the shared dialog primitive. The change is covered by targeted rendering tests and is ready to merge.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mcpjam-inspector/client/src/components/__tests__/design-system-portal-translate.test.tsx">

<violation number="1" location="mcpjam-inspector/client/src/components/__tests__/design-system-portal-translate.test.tsx:204">
P2: These three sub-content tests open the submenu with `fireEvent.click`, but Radix submenus open on pointer-enter/hover or keyboard, not click, and `fireEvent.click` fires no pointer events in jsdom. The sub content never renders, so `expectOptedOutOfTranslation` fails at its `not.toBeNull()` check, making the tests unreliable rather than testing the opt-out. Use `userEvent.hover` (await user.hover(screen.getByText("More"))) to open each submenu, matching the repo's existing pattern in model-selector.test.tsx.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

</DropdownMenuContent>
</DropdownMenu>,
);
fireEvent.click(screen.getByText("More"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: These three sub-content tests open the submenu with fireEvent.click, but Radix submenus open on pointer-enter/hover or keyboard, not click, and fireEvent.click fires no pointer events in jsdom. The sub content never renders, so expectOptedOutOfTranslation fails at its not.toBeNull() check, making the tests unreliable rather than testing the opt-out. Use userEvent.hover (await user.hover(screen.getByText("More"))) to open each submenu, matching the repo's existing pattern in model-selector.test.tsx.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcpjam-inspector/client/src/components/__tests__/design-system-portal-translate.test.tsx, line 204:

<comment>These three sub-content tests open the submenu with `fireEvent.click`, but Radix submenus open on pointer-enter/hover or keyboard, not click, and `fireEvent.click` fires no pointer events in jsdom. The sub content never renders, so `expectOptedOutOfTranslation` fails at its `not.toBeNull()` check, making the tests unreliable rather than testing the opt-out. Use `userEvent.hover` (await user.hover(screen.getByText("More"))) to open each submenu, matching the repo's existing pattern in model-selector.test.tsx.</comment>

<file context>
@@ -0,0 +1,248 @@
+        </DropdownMenuContent>
+      </DropdownMenu>,
+    );
+    fireEvent.click(screen.getByText("More"));
+
+    expectOptedOutOfTranslation("dropdown-menu-sub-content");
</file context>
Suggested change
fireEvent.click(screen.getByText("More"));
await user.hover(screen.getByText("More"));

@Paoli99
Paoli99 self-requested a review September 8, 2026 20:27
@Paoli99

Paoli99 commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Code Review Finding: PR #4704

Found 1 issue with test reliability:

Unreliable Submenu Tests (P2)

File: design-system-portal-translate.test.tsx

Problem: Three submenu tests use fireEvent.click() to open submenus, but Radix submenus respond to pointer-enter/hover or keyboard events, NOT click events.

Why tests are unreliable:

  • fireEvent.click() fires no pointer events in jsdom
  • Submenus never actually render
  • Tests check for translate="no" on an element that was never rendered
  • Tests appear to pass when they're actually testing nothing (false negatives)

Solution: Use userEvent.hover() to open submenus, matching the existing pattern in model-selector.test.tsx:

// Instead of:
fireEvent.click(screen.getByText("More"));

// Use:
await user.hover(screen.getByText("More"));

Affected tests:

  • DropdownMenuContent submenu test
  • SelectContent submenu test
  • ContextMenuContent submenu test

This makes the test coverage for submenu translation protection unreliable. The fix is to use the correct event type that Radix actually responds to.

This branch was successfully deployed

1 active deployment
preview-pr-4704 — 45e3be17 Deployed Sep 4, 2026 by nachocossio via upsert-preview #17357
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.

3 participants