Skip to content

fix(web): MCP App cards no longer flash white in dark mode while the app loads - #33352

Merged
juliusmarminge merged 6 commits into
mainfrom
fix/issue-33303
Nov 11, 2026
Merged

juliusmarminge merged 6 commits into
mainfrom
fix/issue-33303

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

Problem

In dark mode, MCP App cards on web and desktop are bright white blocks from the moment the frame loads until the app sends ui/initialize. For apps with a large bundle that is seconds. The <iframe> gets color-scheme: dark from the host, but the app document has no color scheme until the theme bootstrap hears from the host, so Chromium paints the child canvas opaque white.

Reproduced on main 1a43f4870d in the web app: a Claude thread called a synthetic MCP tool whose ui:// app sends ui/initialize 4 s after load. In dark mode both cards stayed white for about 4 s after switching back to the thread, and again after a full page reload. Light mode was unaffected.

Change

  • McpAppFrame adds a t3-color-scheme=<light|dark> query parameter to the frame URL when it first adopts it. The frame still keeps that one URL; later theme changes keep flowing through ui/notifications/host-context-changed.
  • The theme bootstrap that the server already injects into every app document reads that parameter and appends <meta name="color-scheme" content="…"> to the app's head before the app's own scripts run and before first paint. It uses a meta tag rather than an inline style on <html>, which would replace an app's own <html style> attribute. The captured fixture app sets no styles or color scheme of its own, so an app that sets color-scheme in its own CSS is not shown below.

The server route ignores the query string (the asset token is in the path), so older servers keep today's behaviour. Mobile sets no color scheme on its inner frame and is unchanged.

Scope and approval

Fixes #33303

Verification

  • vp test run packages/shared/src/mcpApp.test.ts apps/server/src/http.test.ts: 39 passed, including a new test that the bootstrap applies only light/dark from the frame URL before any message arrives.
  • At e829b7b38f (main merged; upstream changes touch only mobile files): vp test run packages/shared/src/mcpApp.test.ts 15 passed; vp run check:changed: format, lint, knip, typecheck (35 packages), tests and React compiler all pass; CI 35/35 green.
  • Review at e829b7b38f: two independent reviewers, both merge, no findings. Both name the same trade-off, row C6 below. Neither change touches auth, sandboxing, CSP or the MCP tool surface; the parameter parse is linear (about 2 ms on 3000 malformed tokens).
  • Proof audit at e829b7b38f: all 57 PNGs and 10 recordings inspected; every delivered diff image recomputes exactly. C2–C5 are sufficient. An independent rebuild of C1 paints both cards dark before ui/initialize, as captured, but differs by 5,977 pixels: its seed turn (a real provider turn) wrote an extra "Worked for" row that moves the cards down 42 px. The C6 frames show the loading text losing contrast in the pre-initialize window, as described.
  • Real web app, headless Chromium 148, 1400×1022, DPR 1, fresh databases per side, a synthetic MCP app that initializes after 4 s. Captures at 807635d674; the later commits on the branch are a comment wording change and a merge of main that does not touch the changed files.

C1, T3 dark on a light browser preference, about 2 s after returning to the thread:

Before After Diff0 DiffT
C1-before.png
1a43f4870d
C1-after.png
807635d674
C1-diff0.png
1a43f4870d ↔ 807635d674
C1-diffT.png
1a43f4870d ↔ 807635d674

Recordings: C1-before.mp4 · C1-after.mp4

Diff0 is threshold 0, DiffT threshold 8/255, both amplified 32×. Only the two app canvases change; there are zero changed pixels outside them at threshold 0.

Row Result Sheet
C1, T3 dark / browser light white cards before, dark after C1
C2, T3 light / browser dark zero changed pixels C2
C3, T3 system / browser dark white cards before, dark after C3
C4, T3 system / browser light zero changed pixels C4
C5, theme switch after load, close and Show app follows the theme without reloading; reopen uses the current theme C5
C6, theme switch before ui/initialize keeps the mount-time scheme until initialization; loading text can lose contrast in that window Light→Dark · Dark→Light

Full evidence index with every recording, trace and timestamp: Evidence.md. Fixture matrix: fixture-matrix.md. Seeding and driver scripts, including the synthetic MCP server: seeding-and-drivers.zip.

Not checked: the Electron desktop app itself (Chromium covers its render path), Safari, native mobile (unchanged code path), and a real Datadog app.

AI-assisted; verified with the tests and tools listed above.

🤖 Generated with Claude Code

juliusmarminge and others added 3 commits November 9, 2026 11:14
…app loads

The frame element declared the host's dark color scheme, but the app
document declared none until `ui/initialize` answered, so Chromium painted
the child canvas opaque white for as long as the app took to initialize.

The frame URL now carries the host's scheme in a `t3-color-scheme` query
parameter, and the injected theme bootstrap applies it before first paint.
Later theme changes still arrive through host-context-changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Nov 9, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 6d93911

Macroscope's review found this PR approvable — MCP App frames receive the existing host theme before initialization, preventing the white loading flash in dark mode. The change is limited to initial presentation and preserves subsequent theme updates.

No code changes detected at 7f2ac6d. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.5 KiB 21.5 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 1 1 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Baseline: 5be6bc9 · PR result: 7f2ac6d · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

Run configuration

Commits

Reviewing files that changed from the base of the PR and between e829b7b and 7f2ac6d.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review

No actionable comments were generated in the recent review.

Walkthrough

The MCP App frame URL now includes the host’s current theme appearance. The theme bootstrap reads that parameter before host initialization and sets a color-scheme meta tag for light or dark.

Changes: MCP App theme initialization

Layer / File(s) Summary
Bootstrap color scheme from frame URL
packages/shared/src/mcpApp.ts, packages/shared/src/mcpApp.test.ts
The bootstrap reads the t3-color-scheme parameter and adds a color-scheme meta tag when its value is light or dark. Tests cover those values, normal, and a missing parameter.
Pass host appearance to frame
apps/web/src/components/chat/McpAppFrame.tsx
When the asset succeeds and the frame source is unset, the frame adds the current theme appearance to the asset URL before setting its source.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Suggested labels: bugfix

Suggested reviewers: t3dotgg, macroscopeapp

Merge Risk: Minimal · previous review 6d939

Web frames receive the initial theme; mobile behavior remains unchanged. No material merge blocker was identified.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the actual change: MCP App cards stop flashing white in dark mode while the app loads. It is specific and follows the conventional commit form. The raw summary, PR objectives, and link…
Linked Issues check Passed Directly linked issue #33303 requires that MCP App cards not paint a white canvas in dark mode before the app sends ui/initialize. Its suggested approaches are a themed placeholder or setting the colo…
Out of Scope Changes check Passed The whole-PR changes touch three files in the reviewed diff: packages/shared/src/mcpApp.ts, packages/shared/src/mcpApp.test.ts, and apps/web/src/components/chat/McpAppFrame.tsx. Each change connects t…
Description check Passed The description covers the problem, change, linked issue, verification results, UI evidence, and untested areas. It does not identify explicit maintainer approval of the scope or explain why the fix q…
Full details: Title check

Explanation

The title names the actual change: MCP App cards stop flashing white in dark mode while the app loads. It is specific and follows the conventional commit form. The raw summary, PR objectives, and linked issue #33303 all describe this same fix, so the title matches the changeset.

Full details: Linked Issues check

Explanation

Directly linked issue #33303 requires that MCP App cards not paint a white canvas in dark mode before the app sends ui/initialize. Its suggested approaches are a themed placeholder or setting the color scheme before first paint. The PR uses the second approach.

Code evidence at head:

  • apps/web/src/components/chat/McpAppFrame.tsx: when the frame first adopts its URL, the code sets t3-color-scheme=<theme.appearance> on the asset URL. The URL is still set only once, so later theme changes use ui/notifications/host-context-changed as before.
  • packages/shared/src/mcpApp.ts: injectMcpAppThemeBootstrap reads MCP_APP_COLOR_SCHEME_PARAM from location.search. When the value is light or dark, it appends <meta name="color-scheme"> to document.head. The bootstrap script is placed after the doctype and optional CSP meta, before any app content or app script, so the parser places it in head before app scripts run.
  • packages/shared/src/mcpApp.test.ts: a new test runs the bootstrap in a VM with a mocked document. It asserts that dark and light apply a meta tag, and that normal or a missing parameter applies none.

The fix does not depend on the host message, so it covers the window before ui/initialize, which is the scope of #33303.

Uncertainty: I did not verify in code that theme.appearance resolves to light or dark and not system. The author reports headless Chromium captures for system and dark theme rows (C1, C3) that show the fix. Those captures are author-reported and I did not reproduce them. The unit test runs against a mocked document, not a real browser. Residual limitation (author-reported C6): a theme change before ui/initialize keeps the mount-time scheme. This is not a stated requirement of #33303.

Full details: Out of Scope Changes check

Explanation

The whole-PR changes touch three files in the reviewed diff: packages/shared/src/mcpApp.ts, packages/shared/src/mcpApp.test.ts, and apps/web/src/components/chat/McpAppFrame.tsx. Each change connects to #33303:

  • McpAppFrame.tsx: adds the color-scheme query parameter to the initial frame URL.
  • mcpApp.ts: adds the MCP_APP_COLOR_SCHEME_PARAM constant and the bootstrap logic that reads it and applies the meta tag.
  • mcpApp.test.ts: tests the new bootstrap behavior.

No unrelated change is present in the reviewed diff. The PR description states that the server route, mobile inner frame, and Electron app are unchanged. I did not inspect the server route or mobile code, so those statements are not verified here. They do not affect this verdict, because no server or mobile file appears in the whole-PR change set.

Full details: Description check

Explanation

The description covers the problem, change, linked issue, verification results, UI evidence, and untested areas. It does not identify explicit maintainer approval of the scope or explain why the fix qualifies without that approval.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 4f0b74e and 6d93911.

Files selected for processing: 3

  • apps/web/src/components/chat/McpAppFrame.tsx
  • packages/shared/src/mcpApp.test.ts
  • packages/shared/src/mcpApp.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

@juliusmarminge juliusmarminge added bugfix Fix PR opened by an issue lead; enters the PR loop pr:gate-failed A land gate failed pr:in-progress A lead is working on this PR and removed pr:gate-failed A land gate failed labels Nov 9, 2026
@juliusmarminge juliusmarminge added pr:gate-failed A land gate failed pr:in-progress A lead is working on this PR and removed pr:in-progress A lead is working on this PR pr:gate-failed A land gate failed labels Nov 10, 2026
@juliusmarminge juliusmarminge added pr:gate-failed A land gate failed and removed pr:in-progress A lead is working on this PR labels Nov 10, 2026

Copy link
Copy Markdown
Member Author

Evidence

Both captures: real web dev servers, Chromium 157 on Linux, dark appearance, 1280×800 CSS viewport, DPR 2, separate fresh databases and identical synthetic data.

Steps: seed a synthetic ui://apps/dataviz MCP App that delays ui/initialize by four seconds; open its thread; reload the page; observe through initialization. Tool output was replayed through the real adapter and orchestration. The attachment route, chat card and sandboxed iframe used each commit's production code.

Before After
5be6bc99c2 7f2ac6d0e5
Before: white MCP App canvas before initialization After: dark MCP App canvas before initialization
Reload recording Reload recording
White during the delay; dark after initialization. Dark from its first visible paint through initialization.

Stills were verified against identical screenshots at least 500 ms apart. Recordings and stills were visually inspected before upload.

Reproduction files: HTML/RPC seed, replay fixture generator, turn driver, capture driver, environment and instructions.

AI-assisted; verified with the tests and tools listed above.

Not shown:

  • Live Claude/Datadog tool acquisition was not exercised; proof uses synthetic completed MCP tool output and a delayed synthetic app.
  • Electron desktop and mobile were not run; captures cover the real web client.
  • Thread switching and changing themes after initialization were not exercised; the matched interaction was page reload.

@juliusmarminge
juliusmarminge merged commit ccc7b2d into main Nov 11, 2026
35 checks passed
@juliusmarminge
juliusmarminge deleted the fix/issue-33303 branch November 11, 2026 02:58
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Nov 11, 2026
**v0.1.9-nightly.20261111.4501** · 17 changes from 3 contributors

- **Separate Claude resets.** Choose between Fable and weekly resets without spending the wrong allowance.
- **Reuse voice models.** Set up local speech-to-text faster by reusing compatible models already on your computer.

### Plus 11 more bug fixes.

<details>
<summary>Full changelog</summary>

## What's Changed
* fix(mobile): attaching larger images no longer crashes with a BigInt error by @juliusmarminge in pingdotgg/t3code#34087
* feat(mobile): pick a voice environment once, then its method by @juliusmarminge in pingdotgg/t3code#34073
* fix(server): a t3 thread send to a finished delegated child notifies its parent by @juliusmarminge in pingdotgg/t3code#33446
* fix(web): question panel header text sits centered in its row by @juliusmarminge in pingdotgg/t3code#34095
* feat(scripts): scenarios prove a fix in the real app with e2e, on web and iOS by @juliusmarminge in pingdotgg/t3code#34007
* feat(claude): Fable resets are offered and redeemed apart from weekly resets by @juliusmarminge in pingdotgg/t3code#34106
* fix(desktop): desktop IPC only answers the main app window by @juliusmarminge in pingdotgg/t3code#34094
* feat(server): voice models reuse copies already on disk by @juliusmarminge in pingdotgg/t3code#34096
* fix(web): scroll project picker to top on first result by @Yash-Singh1 in pingdotgg/t3code#34113
* feat(opencode): track native image job outcomes and previews by @Yash-Singh1 in pingdotgg/t3code#34089
* fix(scripts): scenarios run where localhost resolves to IPv6 first by @juliusmarminge in pingdotgg/t3code#34121
* ci: browser secret helper apt step fetches fewer indexes by @t3dotgg in pingdotgg/t3code#33966
* fix(client-runtime): a search query pasted from github.com lists pull requests again by @juliusmarminge in pingdotgg/t3code#30150
* fix(web): tighter outdated-conversation cards in the PR code panel by @Yash-Singh1 in pingdotgg/t3code#34140
* fix(web): MCP App cards no longer flash white in dark mode while the app loads by @juliusmarminge in pingdotgg/t3code#33352
* fix(peer): link requests use the addresses the app already has for a machine by @t3dotgg in pingdotgg/t3code#34139
* fix(server): Find frees searched thread text after one idle minute by @t3dotgg in pingdotgg/t3code#34132


**Full Changelog**: pingdotgg/t3code@v0.1.9-nightly.20261110.4491...v0.1.9-nightly.20261111.4501

</details>


Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.1.9-nightly.20261111.4501
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop pr:gate-failed A land gate failed size:S 10-29 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP App cards flash white in dark mode while the app loads

1 participant