Skip to content

fix(t3team): show_widget live schema carries the theme contract; resnapshot widget iframe theme on host flips - #200

Merged
johnnyelwailer merged 2 commits into
mainfrom
fix/widget-theme-contract-live-tool
Sep 14, 2026
Merged

johnnyelwailer merged 2 commits into
mainfrom
fix/widget-theme-contract-live-tool

Conversation

@johnnyelwailer

Copy link
Copy Markdown
Owner

Problem

show_widget-produced widgets rendered with a hard-coded dark palette regardless of the host theme — and stayed frozen on the mount-time palette after any light/dark flip.

Two independent defects:

  1. The model never saw the widget authoring contract. The theme-token + icon-sprite contract (use var(--*) host theme variables for every color, use the t3w-icon sprite, never ship a palette) was codified in packages/project-context as T3TEAM_WIDGET_AUTHORING_GUIDANCE with a doc that explicitly names "the MCP tool's widget_code input schema" as a carrying surface — but the live Tool.make("t3team_show_widget", ...) wrapper had shipped with a bare description and widget_code: Schema.String since the MCP toolkit was carved out of the catalog on 2026-07-19 (03953ee "expose inline widgets to provider agents"). The Aug 29 guidance-constant work wired the contract into the catalog snapshot and the help topic, and tests locked the catalog — but the live schema was never wired, and no test locked the live surface.

  2. The host theme snapshot was taken once, at mount. The widget srcdoc cannot inherit host CSS custom properties, so the theme is snapshotted into the iframe's own :root at build time — but the srcdoc memo depended only on [widget.html, nonce], so after a light/dark flip (user toggle, or OS change under system-follow) the widget stayed on the old palette while the host re-themed around it.

Changes

Fix 1 — live schema carries the contract (62c49e3)

  • T3TEAM_WIDGET_SHOW_TOOL_DESCRIPTION added to t3teamWidgetGuidance.ts as a second shared constant (single source of truth; the "two surfaces must not restate" invariant is preserved — the guidance doc now enumerates the constants instead of restating the text).
  • apps/server/.../t3team/tools.ts: live tool description + title/widget_code property annotations now use the shared constants via Schema.annotate({ description }) (the established Effect-4 pattern in this file).
  • t3team-mcpToolInputSchema.test.ts: three new assertions lock the live JSON schema (not just the catalog snapshot) to the documented contract — the class of drift that produced this bug is now a red build.

Fix 2 — re-snapshot on theme flips (4d06e84)

  • New useThemeSnapshot() export in apps/web/src/hooks/useTheme.ts: the raw ThemeSnapshot with referential stability (invalidated only when the resolved theme actually changes). useTheme() allocates a fresh object per render and would defeat the memo — the new test caught exactly that.
  • t3team-useWidgetBlockController.ts: the srcdoc memo now depends on the stable snapshot → widgets rebuild exactly on real theme flips, zero rebuilds on plain re-renders.
  • New t3team-widgetBlockThemeResync.test.tsx: mount-snapshot, exactly-one-rebuild on light→dark flip (verified in the live iframe DOM), no rebuild on plain re-render.

Verification

  • packages/project-context: typecheck + guidance/catalog tests green.
  • apps/server: 25/25 MCP tool input-schema tests (incl. the 3 new live-schema locks), 25/25 t3team toolkit + widget-show tests green.
  • apps/web: web typecheck green; 3/3 new resync tests; 22/22 pre-existing widget-suite tests green.
  • End-to-end on a fixed instance (standalone src/t3team-bin.ts serve from this branch, scratch project, Nexplore provider): two fresh threads asked to visualize things with zero design hints (no mention of themes, colors, tokens, icons, or layout). Both agents' t3team_show_widget calls (4 total, verified in provider event logs) produced contract-clean widgets: HTML fragment (no <!DOCTYPE>), zero hard-coded hex colors, 12 host theme tokens each, t3w-icon sprite, no emoji glyphs — and neither agent called the help topic, so the contract reached them purely through the live tool schema. Pre-fix control: this conversation's own orchestrator widget hard-coded 8+ hex values with the old schema.

Notes

  • Greenfield-style: no compatibility fallbacks; the catalog entry now points at the same constant instead of a duplicated string.
  • The pre-existing widget-guidance help topic is unchanged — it remains the on-demand deep reference.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M labels Sep 11, 2026
Phil J added 2 commits September 12, 2026 18:00
…on-sprite contract

The widget authoring guidance (T3TEAM_WIDGET_AUTHORING_GUIDANCE) lived only in
the catalog snapshot and the t3team_help('widget-guidance') topic; the live
MCP tool shipped a bare description and Schema.String properties with no
descriptions, so agents never saw the rule 'never hard-code light or dark
palette colors' and rendered widgets with fixed hex palettes that clashed
with the host theme.

- Move the tool-level description into a shared constant
  (T3TEAM_WIDGET_SHOW_TOOL_DESCRIPTION) used by both the live toolkit and
  the catalog entry — one source of truth, no restated copy.
- Annotate widget_code with the full authoring contract and title with the
  artifact-name rule in the live Tool.make schema.
- Lock the live JSON schema to the documented contract in
  t3team-mcpToolInputSchema.test.ts so the surfaces cannot drift again.
The widget srcdoc snapshots the host's theme CSS variables at build time
(a sandboxed iframe :root cannot inherit them), but the snapshot was
memoized on [widget.html, nonce] — so after any light/dark flip (user
toggle or OS change under system-follow) widgets stayed frozen on the
mount-time palette while the host re-themed around them.

- Subscribe the controller to the theme store via a new
  useThemeSnapshot() export: referentially stable, invalidated only when
  the resolved theme actually changes. (useTheme() allocates a fresh
  object per render and would have defeated the memo — the resync test
  caught exactly that.)
- New test locks the behavior: one snapshot on mount, exactly one
  rebuild on a light→dark flip reaching the live iframe, zero rebuilds
  on plain re-renders.
@johnnyelwailer
johnnyelwailer force-pushed the fix/widget-theme-contract-live-tool branch from db75e77 to 8b2d7b4 Compare September 12, 2026 16:07
@johnnyelwailer
johnnyelwailer merged commit 9ab82db into main Sep 14, 2026
12 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 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.

1 participant