---
phase: 15-wasm-widget-bridge
plan: 02
type: execute
wave: 2
depends_on: [15-01]
files_modified:
  - crates/mcp-preview/assets/index.html
  - crates/mcp-preview/assets/widget-runtime.js
autonomous: true
requirements: [WASM-01, WASM-02, WASM-04, WASM-05]

must_haves:
  truths:
    - "Developer can toggle between Proxy and WASM bridge modes in the preview header"
    - "Toggling to WASM triggers a build (if needed) and reloads the widget with WASM bridge injected"
    - "Widget code calling window.mcpBridge.callTool() works identically in both bridge modes"
    - "Bridge calls appear in DevTools Network tab regardless of mode (transparent, no mode tags)"
    - "Standalone widget-runtime.js loads WASM client and exposes window.mcpBridge outside preview context"
    - "Default mode is Proxy -- WASM requires explicit toggle"
  artifacts:
    - path: "crates/mcp-preview/assets/index.html"
      provides: "Proxy/WASM toggle button, wrapWidgetHtmlWasm(), build progress UI"
      contains: "wrapWidgetHtmlWasm"
    - path: "crates/mcp-preview/assets/widget-runtime.js"
      provides: "Standalone WASM bridge polyfill for use outside preview"
      contains: "mcpBridgeReady"
  key_links:
    - from: "crates/mcp-preview/assets/index.html"
      to: "/api/wasm/build"
      via: "fetch in toggleBridgeMode()"
      pattern: "api/wasm/build"
    - from: "crates/mcp-preview/assets/index.html"
      to: "/wasm/mcp_wasm_client.js"
      via: "ES module import in wrapWidgetHtmlWasm()"
      pattern: "wasm/mcp_wasm_client"
    - from: "crates/mcp-preview/assets/widget-runtime.js"
      to: "mcp_wasm_client.js"
      via: "dynamic import"
      pattern: "import.*mcp_wasm_client"
---

<objective>
Add the Proxy/WASM bridge toggle to the preview UI and create the standalone widget-runtime.js bundle.

Purpose: The developer needs a one-click toggle to switch between proxy bridge (simple, no build step) and WASM bridge (direct connection, no proxy middleman). The WASM bridge adapter must normalize responses to exactly match the proxy bridge's window.mcpBridge API. The standalone widget-runtime.js enables WASM bridge usage outside the preview context.

Output: Updated preview UI with toggle button and WASM bridge injection, plus widget-runtime.js standalone polyfill.
</objective>

<execution_context>
@./.claude/get-shit-done/workflows/execute-plan.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/14-preview-bridge-infrastructure/14-02-SUMMARY.md
@.planning/phases/15-wasm-widget-bridge/15-RESEARCH.md
@.planning/phases/15-wasm-widget-bridge/15-01-SUMMARY.md
@crates/mcp-preview/assets/index.html
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add Proxy/WASM toggle and WASM bridge injection to preview UI</name>
  <files>crates/mcp-preview/assets/index.html</files>
  <action>
Modify the preview UI to support WASM bridge mode alongside the existing proxy bridge.

**1. Add toggle button in the preview header:**
- Place a segmented toggle button next to the existing header controls: two options "Proxy" and "WASM"
- Default state: "Proxy" is highlighted/active
- Styling: pill-shaped segmented control. Active segment has a solid background color (e.g., `#4a9eff` for Proxy, `#7c3aed` for WASM). Inactive segment is transparent with border.
- The toggle is disabled during WASM build (shows "Building..." text in the WASM segment with a subtle animation)

**2. Add `bridgeMode` property to PreviewRuntime:**
- `this.bridgeMode = 'proxy';` -- initialized in constructor
- `this.wasmReady = false;` -- tracks whether WASM artifacts are cached

**3. Implement `toggleBridgeMode(mode)` method on PreviewRuntime:**
```javascript
async toggleBridgeMode(mode) {
  if (mode === this.bridgeMode) return;
  this.bridgeMode = mode;
  this.updateToggleUI();

  if (mode === 'wasm') {
    if (!this.wasmReady) {
      this.setToggleBuilding(true);
      try {
        const resp = await fetch('/api/wasm/build', { method: 'POST' });
        const data = await resp.json();
        if (data.status === 'ready') {
          this.wasmReady = true;
        } else {
          // Show error, revert to proxy
          this.showBuildError(data.error || 'WASM build failed');
          this.bridgeMode = 'proxy';
          this.updateToggleUI();
          return;
        }
      } catch (e) {
        this.showBuildError(e.message);
        this.bridgeMode = 'proxy';
        this.updateToggleUI();
        return;
      } finally {
        this.setToggleBuilding(false);
      }
    }
  }

  // Reload the active widget with the new bridge mode
  if (this.activeResourceUri) {
    await this.loadResourceWidget(this.activeResourceUri);
  }
}
```

**4. Implement `wrapWidgetHtmlWasm(html)` method on PreviewRuntime:**
This is the WASM equivalent of the existing `wrapWidgetHtml(html)` method. It injects WASM bridge code instead of proxy bridge code.

Key implementation from research:
- Use `<script type="module">` in the iframe srcdoc
- Import `init` and `WasmClient` from `/wasm/mcp_wasm_client.js`
- Call `await init('/wasm/mcp_wasm_client_bg.wasm')`
- Create WasmClient, connect to MCP server URL (from `this.mcpUrl`)
- Expose `window.mcpBridge` with the SAME API as the proxy bridge:
  - `callTool(name, args)` -- calls `client.call_tool(name, args)`, normalizes response to `{ success: !mcpResult.isError, content: mcpResult.content || [], _meta: mcpResult._meta || null }`, logs via `preview.logBridgeCall()`
  - `getState()` -- returns `preview.widgetState`
  - `setState(s)` -- merges into `preview.widgetState`, calls `preview.updateStateView()`
  - `sendMessage(msg)` -- logs via `preview.logConsole()`
  - `openExternal(url)` -- opens in new tab
  - `theme`, `locale`, `displayMode` -- getters from preview runtime
- Also set `window.openai = window.mcpBridge` (ChatGPT compatibility alias)
- Fire `preview.logEvent('wasmBridgeReady', {})` after connection

CRITICAL: The bridge adapter MUST normalize WASM responses to exactly match proxy bridge output shape. The WASM `call_tool()` returns MCP CallToolResult (with `content[]` and `isError`), but the proxy bridge returns `{ success, content, _meta }`. The adapter normalizes this. This is WASM-02.

CRITICAL: Bridge calls in DevTools must look IDENTICAL regardless of mode -- no "[WASM]" or "[Proxy]" prefixes. Both modes call `preview.logBridgeCall()` with the same arguments.

**5. Modify existing `wrapWidgetHtml()` to dispatch based on bridge mode:**
Rename the existing `wrapWidgetHtml(html)` to `wrapWidgetHtmlProxy(html)`. Create a new `wrapWidgetHtml(html)` that dispatches:
```javascript
wrapWidgetHtml(html) {
  if (this.bridgeMode === 'wasm') {
    return this.wrapWidgetHtmlWasm(html);
  }
  return this.wrapWidgetHtmlProxy(html);
}
```

**6. Add toggle UI helper methods:**
- `updateToggleUI()` -- updates active/inactive state of toggle segments
- `setToggleBuilding(isBuilding)` -- shows/hides building indicator in WASM segment
- `showBuildError(message)` -- displays error inline (e.g., below the toggle or as a toast)

**7. Check WASM status on startup:**
On `initSession()`, fire `fetch('/api/wasm/status')` to check if WASM artifacts are already cached. If status is "ready", set `this.wasmReady = true`. This enables instant toggling if the developer has previously built.
  </action>
  <verify>
Open `crates/mcp-preview/assets/index.html` and verify: (1) toggle button HTML exists in the header, (2) `wrapWidgetHtmlWasm` method exists, (3) `wrapWidgetHtmlProxy` method exists (renamed from original), (4) `toggleBridgeMode` method exists with `/api/wasm/build` fetch, (5) `bridgeMode` property initialized to 'proxy'. `cargo check -p mcp-preview` compiles (no Rust changes in this task, but verify the HTML is valid embedded content). Search for "[WASM]" or "[Proxy]" text tags in bridge call logging -- must find none.
  </verify>
  <done>Preview UI has a Proxy/WASM toggle button. Clicking WASM triggers build if needed, then reloads widget with WASM bridge. Bridge adapter normalizes responses to match proxy bridge exactly. DevTools logging is identical in both modes. Default is Proxy.</done>
</task>

<task type="auto">
  <name>Task 2: Create standalone widget-runtime.js for WASM bridge outside preview</name>
  <files>crates/mcp-preview/assets/widget-runtime.js</files>
  <action>
Create a standalone JavaScript file that widget authors can include in their HTML to get `window.mcpBridge` powered by the WASM MCP client. This file works OUTSIDE the preview context (no `window.parent.previewRuntime` dependency).

**Create `crates/mcp-preview/assets/widget-runtime.js`:**

The file should:

1. Find its own script tag to read the `data-mcp-url` attribute:
   ```javascript
   const scriptTag = document.currentScript || document.querySelector('script[data-mcp-url]');
   const serverUrl = scriptTag?.getAttribute('data-mcp-url');
   ```

2. If `data-mcp-url` is missing, log error and return early (do not throw).

3. Determine WASM artifact URLs relative to the script's own location:
   ```javascript
   const scriptUrl = new URL(scriptTag.src);
   const wasmJsUrl = new URL('mcp_wasm_client.js', scriptUrl);
   const wasmBinaryUrl = new URL('mcp_wasm_client_bg.wasm', scriptUrl);
   ```

4. Dynamically import the WASM JS module, call `init()` with the WASM binary URL, create a `WasmClient`, and connect to the server URL.

5. Expose `window.mcpBridge` with standalone implementations (no preview runtime dependency):
   - `callTool(name, args)` -- calls `client.call_tool()`, normalizes to `{ success, content }` shape
   - `getState()` -- returns `window.__mcpState || {}`
   - `setState(s)` -- merges into `window.__mcpState`
   - `sendMessage(msg)` -- `console.log('[mcpBridge] sendMessage:', msg)`
   - `openExternal(url)` -- `window.open(url, '_blank')`
   - `theme` getter -- reads `document.documentElement.dataset.theme || 'light'`
   - `locale` getter -- `navigator.language`
   - `displayMode` getter -- `'inline'`

6. Also set `window.openai = window.mcpBridge` (ChatGPT compatibility).

7. Dispatch `window.dispatchEvent(new Event('mcpBridgeReady'))` after successful connection.

8. Handle errors gracefully: if WASM fails to load or connection fails, dispatch `window.dispatchEvent(new CustomEvent('mcpBridgeError', { detail: { error: errorMessage } }))`.

9. Wrap the entire file in an async IIFE: `(async function() { ... })();`

**Usage pattern (documented in comments at top of file):**
```html
<script src="widget-runtime.js" data-mcp-url="http://localhost:3000/mcp"></script>
<script>
  window.addEventListener('mcpBridgeReady', async () => {
    const result = await window.mcpBridge.callTool('my_tool', { arg: 'value' });
  });
</script>
```

**Ensure the file is served by the preview server:**
The existing `assets.rs` in mcp-preview uses `rust-embed` to serve files from the `assets/` directory. Since widget-runtime.js goes in `assets/`, it will be automatically served at `/assets/widget-runtime.js`. The preview server should also serve it at `/wasm/widget-runtime.js` for consistency with the WASM artifact path. Verify the existing asset handler will pick it up, or note if a route alias is needed (the executor can add one in server.rs if needed).

The file should be well-commented to serve as documentation for widget authors.
  </action>
  <verify>
File `crates/mcp-preview/assets/widget-runtime.js` exists. Contains `data-mcp-url`, `mcpBridgeReady`, `window.mcpBridge`, `callTool`, `getState`, `setState`, `sendMessage`, `openExternal`. Does NOT contain `window.parent.previewRuntime` (standalone -- no preview dependency). Contains `window.openai = window.mcpBridge`. `cargo check -p mcp-preview` still compiles (rust-embed picks up new asset automatically).
  </verify>
  <done>widget-runtime.js exists as a standalone drop-in `window.mcpBridge` polyfill. Widget authors include it with `<script src="widget-runtime.js" data-mcp-url="..."></script>` and listen for `mcpBridgeReady`. The file has no preview runtime dependency and works in any HTML page.</done>
</task>

</tasks>

<verification>
1. `cargo check -p mcp-preview` -- compiles successfully
2. `cargo clippy -p mcp-preview -- -D warnings` -- zero warnings
3. Open preview UI: toggle button visible in header showing "Proxy" (active) and "WASM"
4. `grep "wrapWidgetHtmlWasm" crates/mcp-preview/assets/index.html` -- method exists
5. `grep "wrapWidgetHtmlProxy" crates/mcp-preview/assets/index.html` -- renamed original exists
6. `grep "bridgeMode" crates/mcp-preview/assets/index.html` -- property initialized to 'proxy'
7. `grep "mcpBridgeReady" crates/mcp-preview/assets/widget-runtime.js` -- event dispatched
8. `grep "data-mcp-url" crates/mcp-preview/assets/widget-runtime.js` -- URL injection via attribute
9. `grep "previewRuntime" crates/mcp-preview/assets/widget-runtime.js` -- zero matches (standalone)
</verification>

<success_criteria>
- Proxy/WASM toggle in preview header with Proxy as default
- Toggling to WASM triggers build (if needed), shows progress, then reloads widget
- WASM bridge adapter normalizes responses to exact proxy bridge shape (WASM-02)
- DevTools logging identical in both modes (no mode tags)
- widget-runtime.js works as standalone polyfill with data-mcp-url attribute (WASM-05)
- Build errors show clear messages (especially missing wasm-pack)
</success_criteria>

<output>
After completion, create `.planning/phases/15-wasm-widget-bridge/15-02-SUMMARY.md`
</output>
