Skip to content

feat(ui): add SPA-side support for WebView2 native bridge#69633

Merged
BunsDev merged 11 commits intoopenclaw:mainfrom
AlexAlves87:feat/webview2-bridge-spa
May 8, 2026
Merged

feat(ui): add SPA-side support for WebView2 native bridge#69633
BunsDev merged 11 commits intoopenclaw:mainfrom
AlexAlves87:feat/webview2-bridge-spa

Conversation

@AlexAlves87
Copy link
Copy Markdown
Contributor

@AlexAlves87 AlexAlves87 commented Apr 21, 2026

Summary

Wires the WebView2 bridge into the SPA lifecycle with a minimal, parity-aligned surface.

  • app-native-bridge.ts reduces NativeBridgeMessage to draft-text (inbound) and ready (outbound handshake). Removes recording-start/stop and voice-start/stop, since there is no corresponding SPA handler or UI surface today, and recording follows the parity decision in openclaw-windows-node#159. NativeBridgeHost now requires only handleChatDraftChange(next).
  • app.ts imports initNativeBridge, calls it in connectedCallback, and cleans it up in disconnectedCallback.
  • draft-text goes through handleChatDraftChange so native-injected text resets input-history navigation the same way a real user edit does.

Context

The native side landed in openclaw-windows-node#192 (c7630fa) with origin validation, dispatcher marshaling, closed-window guards, sanitized logging, and payload JSON validation. This PR closes the SPA side of that minimal bridge surface.

Test plan

  • In a WebView2 host: send {"type":"draft-text","payload":{"text":"hello"}} then chat input updates and input-history navigation resets
  • Disconnect/reconnect component then no listener leak
  • Outside WebView2 (regular browser) then initNativeBridge no-ops without errors

Real behavior proof

  • Behavior or issue addressed: draft-text messages sent from the WebView2 native host were not received by the SPA; the bridge on the SPA side was not wired into the component lifecycle.
  • Real environment tested: OpenClaw Windows Tray (WinUI 3 + WebView2 host), gateway at 127.0.0.1:18789, branch feat/webview2-bridge-spa on top of feat/webview2-bridge in openclaw-windows-node.
  • Exact steps or command run after the patch: (1) Launch Windows tray app; open the chat widget (WebView2 host). (2) Open widget DevTools via right-click then Inspect. (3) Run in the widget console: window.chrome?.webview?.dispatchEvent(new MessageEvent('message',{data:{type:'draft-text',payload:{text:'bridge test'}}})). (4) Close and reopen the widget; repeat rapid re-dispatch to check for listener leaks. (5) Open browser dashboard (not WebView2); verify window.chrome?.webview returns undefined.
  • Evidence after fix: Screen recording https://github.com/AlexAlves87/openclaw/releases/download/proof%2Fwebview2-bridge/Grabacion.de.pantalla.2026-05-08.160245.mp4
  • Observed result after fix: At 00:16 "bridge test" appeared in the chat input after the dispatchEvent call. Rapid re-dispatch (00:28-00:32) produced no duplication. window.chrome?.webview returned undefined in the browser dashboard at 00:43.
  • Not tested: Multi-agent concurrent sessions; native side sending messages before SPA component is mounted.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 28, 2026

Codex review: needs maintainer review before merge.

Summary
Adds a SPA-side WebView2 bridge module, wires it into the OpenClawApp lifecycle, adds bridge/input-history tests, and records the Control UI/Windows change in the changelog.

Reproducibility: not applicable. this is a feature PR rather than a bug report. Current main lacks the SPA bridge, and the PR tests plus inspected WebView2 recording give high-confidence evidence for the proposed behavior.

Real behavior proof
Sufficient (recording): The supplied MP4 proof was inspected and shows after-fix WebView2 draft injection updating the composer, repeated dispatch/reopen behavior without visible duplication, and regular-browser window.chrome?.webview returning undefined.

Next step before merge
No automated repair is needed; the remaining action is normal maintainer review and merge handling for a clean external feature PR.

Security
Cleared: No concrete security or supply-chain concern found; the diff only touches Control UI TypeScript/tests and CHANGELOG.md, adds no dependencies or workflows, and validates a narrow WebView2 message shape before mutating the composer.

Review details

Best possible solution:

Land the narrow draft-text/ready SPA bridge after maintainer review, keeping any broader voice, recording, or pre-mount delivery behavior coordinated with the Windows host bridge proposal.

Do we have a high-confidence way to reproduce the issue?

Not applicable: this is a feature PR rather than a bug report. Current main lacks the SPA bridge, and the PR tests plus inspected WebView2 recording give high-confidence evidence for the proposed behavior.

Is this the best way to solve the issue?

Yes: the PR is the narrowest maintainable SPA counterpart to the merged native bridge, with listener-first setup, cleanup, payload validation, and no unowned voice or recording UI behavior.

Acceptance criteria:

  • pnpm test ui/src/ui/app-native-bridge.test.ts
  • pnpm exec oxfmt --check --threads=1 ui/src/ui/app-native-bridge.ts ui/src/ui/app-native-bridge.test.ts ui/src/ui/app.ts CHANGELOG.md
  • node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.core.json ui/src/ui/app-native-bridge.ts ui/src/ui/app-native-bridge.test.ts ui/src/ui/app.ts
  • pnpm check:changed

What I checked:

  • current_main_no_spa_bridge: Targeted current-main searches found no existing app-native-bridge, initNativeBridge, NativeBridge, chrome.webview, draft-text, or sendToNative implementation in the Control UI; the PR is adding a new SPA capability rather than duplicating current main. (4726755f6a16)
  • webview2_dependency_contract: Microsoft WebView2 documentation confirms window.chrome.webview.addEventListener('message', ...) receives host messages and window.chrome.webview.postMessage(...) sends messages back to the native host, matching the PR's listener plus ready-handshake shape.
  • bridge_contract_and_validation: The PR defines the bridge surface as inbound draft-text and outbound ready, reads raw event data as unknown/object-shaped data, and only updates the composer when payload.text is a string. (ui/src/ui/app-native-bridge.ts:7, c59bb4f5c8ce)
  • lifecycle_wiring_and_cleanup: The PR imports initNativeBridge, stores a cleanup callback, initializes the bridge in connectedCallback, and clears it in disconnectedCallback. (ui/src/ui/app.ts:41, c59bb4f5c8ce)
  • tests_cover_bridge_behavior: The added tests cover WebView2 detection, ready-handshake ordering, regular-browser no-op behavior, valid and malformed draft-text handling, cleanup, and the input-history reset path through the real draft handler. (ui/src/ui/app-native-bridge.test.ts:75, c59bb4f5c8ce)
  • real_behavior_proof_inspected: The supplied MP4 proof was downloaded to scratch and inspected with headless Chrome frames: t=19 shows a WebView2 console dispatch populating the chat composer with bridge test, and t=45 shows a regular browser console returning undefined for window.chrome?.webview. (c59bb4f5c8ce)

Likely related people:

  • BunsDev: GitHub file history shows repeated recent work on ui/src/ui/app.ts and ui/src/ui/app-chat.ts, and earlier commits introduced the dashboard/chat infrastructure and input-history module that this bridge uses. (role: likely Control UI owner and recent maintainer; confidence: high; commits: c5ea6134d041, 37aebf612b83, d12c92c216ab; files: ui/src/ui/app.ts, ui/src/ui/app-chat.ts, ui/src/ui/chat/input-history.ts)
  • Ivocin: Recent merged history specifically hardened the WebChat input-history behavior on the draft/history path that native draft injection reuses. (role: recent input-history behavior maintainer; confidence: medium; commits: 8200d878a340; files: ui/src/ui/chat/input-history.ts, ui/src/ui/app-chat.ts, ui/src/ui/app.ts)
  • steipete: Recent adjacent commits touched chat send behavior, browser realtime/control UI diagnostics, and lifecycle-adjacent app surfaces near this bridge wiring. (role: adjacent Control UI chat and lifecycle maintainer; confidence: medium; commits: 6785633d137c, 826786b114a4, 68359cacbf58; files: ui/src/ui/app.ts, ui/src/ui/app-chat.ts)

Remaining risk / open question:

  • The PR body explicitly leaves multi-agent concurrent sessions and native messages sent before SPA mount untested; I did not find a current diff bug there, but those remain future native-bridge follow-up scope if maintainers need stronger guarantees.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 4726755f6a16.

@AlexAlves87 AlexAlves87 force-pushed the feat/webview2-bridge-spa branch from 7be00dd to 2aefa11 Compare April 29, 2026 00:06
@AlexAlves87 AlexAlves87 marked this pull request as ready for review April 29, 2026 06:33
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 29, 2026

Greptile Summary

This PR wires a WebView2 native bridge into the SPA lifecycle, adding app-native-bridge.ts with initNativeBridge/sendToNative/isWebView2, and integrating it into OpenClawApp.connectedCallback / disconnectedCallback. The implementation is minimal, well-validated, and covered by a thorough test suite.

The surface is correctly scoped to draft-text (inbound) and ready (outbound), listener registration is ordered before the handshake, and cleanup is consistently handled.

Confidence Score: 4/5

Safe to merge — no logic errors or security issues; one minor style note about redundant getWebview() lookup.

The change is small, well-tested, and handles all the relevant edge cases (no-op outside WebView2, cleanup on disconnect, malformed message guards). Only a P2 style observation remains.

No files require special attention.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: ui/src/ui/app-native-bridge.ts
Line: 57

Comment:
**Redundant `getWebview()` lookup via `sendToNative`**

`initNativeBridge` already has a confirmed non-null `bridge` reference, but calling `sendToNative` causes a second `getWebview()` / `window.chrome?.webview` lookup for the same handshake. Consider posting directly on `bridge` to make the intent explicit:

```suggestion
  bridge.postMessage({ type: "ready" });
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat(ui): wire WebView2 bridge — draft-t..." | Re-trigger Greptile

Comment thread ui/src/ui/app-native-bridge.ts Outdated
@AlexAlves87 AlexAlves87 force-pushed the feat/webview2-bridge-spa branch 2 times, most recently from 6bcfa44 to aabd9ab Compare April 30, 2026 09:13
@AlexAlves87
Copy link
Copy Markdown
Contributor Author

I dug into the CI failures and they don't come from the bridge itself. Both visible errors (Node dist test shards failed and Core support boundary shard failed in build-artifacts) pointed to the same shard: checks-node-core-support-boundary.

The root cause was a stale fixture in test/openclaw-npm-postpublish-verify.test.ts that was missing the mirroredRootRuntimeDependencies field now expected by the validator. That was fixed directly on main in bdbce3b1 (fix(ci): align postpublish mirror fixtures), shortly after this PR's CI run.

I've rebased the branch onto current main to pick up that fix. No functional changes to the bridge code or its tests.

@BunsDev BunsDev self-assigned this May 2, 2026
@AlexAlves87
Copy link
Copy Markdown
Contributor Author

@BunsDev sorry for the noise. The root cause was on my side: sendToNative was calling ?.postMessage without a lint suppress and unicorn/require-post-message-target-origin is heuristic, it flags any .postMessage with a single argument regardless of receiver type, so there was no clean way around it without an explicit disable comment.

Pushing a fix now that adds the suppress in sendToNative and restores listener-first ordering with a single ready handshake. All 15 bridge tests pass.

AlexAlves87 and others added 9 commits May 7, 2026 22:33
Adds app-native-bridge.ts and wires it into OpenClawApp lifecycle.

Surface (minimal, parity-aligned with openclaw-windows-node#159):
- inbound: draft-text { payload: { text: string } }
- outbound: ready handshake

Implementation:
- NativeBridgeHost requires only handleChatDraftChange(next).
  recording-start/stop and voice-start/stop excluded — no handler or
  UI surface today, and recording follows the parity decision in
  openclaw-windows-node#159.
- handleNativeMessage validates event.data as unknown: guards object,
  type string, and payload.text string; malformed messages are silently
  ignored.
- draft-text routes through handleChatDraftChange so native-injected
  text resets input-history navigation state, same as a user edit.
- initNativeBridge called in connectedCallback; cleanup in
  disconnectedCallback via private nativeBridgeCleanup field.

Tests (15):
- isWebView2 present/absent
- sendToNative posts message, no-op outside WebView2
- ready handshake sent on init, listener registered first
- no-op outside WebView2
- draft-text happy path calls handleChatDraftChange
- draft-text with missing payload, non-string text — ignored
- unknown types, null, primitives, missing type — ignored
- cleanup removes listener; post-cleanup messages ignored
- integration: draft-text resets active history navigation state

Native side: openclaw-windows-node#192 (c7630fa).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Avoids redundant getWebview() lookup via sendToNative.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bridge.postMessage() triggers oxlint missing-target-origin rule.
sendToNative uses optional chaining (?.postMessage) which is exempt.
The Greptile style suggestion was not lint-safe.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eliminates the eslint-disable-next-line comment so the
lint-suppressions allowlist stays unmodified.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
sendToNative called ?.postMessage without a lint suppress and
unicorn/require-post-message-target-origin is heuristic — it flags
any .postMessage with a single argument regardless of receiver type.
Adds the eslint-disable-next-line comment at the one callsite where
it applies and removes the duplicate ready handshake that had been
inserted before the listener registration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
?.postMessage is exempt from unicorn/require-post-message-target-origin.
The rule was firing on the bare bridge.postMessage() call that was
removed in the previous commit, not on the optional-chaining path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@BunsDev BunsDev force-pushed the feat/webview2-bridge-spa branch from b171ab0 to 0224a82 Compare May 8, 2026 03:38
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 8, 2026
@BunsDev
Copy link
Copy Markdown
Member

BunsDev commented May 8, 2026

Maintainer update: I pushed 0224a826 to rebase this branch onto current main, add the missing CHANGELOG.md entry, and apply the repo formatter's import/line-wrap cleanup on the rebased files.

Verification:

  • pnpm test ui/src/ui/app-native-bridge.test.ts passed locally: 15/15 tests.
  • pnpm exec oxfmt --check --threads=1 ui/src/ui/app-native-bridge.ts ui/src/ui/app-native-bridge.test.ts ui/src/ui/app.ts CHANGELOG.md passed locally.
  • node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.core.json ui/src/ui/app-native-bridge.ts ui/src/ui/app-native-bridge.test.ts ui/src/ui/app.ts passed locally.
  • Blacksmith Testbox pnpm check:changed passed.
  • Exact-head CI is green for 0224a826.

Remaining blocker: Real behavior proof still fails because this external PR does not include an after-fix proof section from a real OpenClaw/WebView2 setup. Please add a Real behavior proof section to the PR body with a real WebView2 host smoke result for the draft-text injection path, cleanup/reconnect path, and regular-browser no-op path. Unit tests/CI are good supplemental evidence, but they do not satisfy this gate by themselves.

@clawsweeper re-review

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 8, 2026

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 8, 2026
@AlexAlves87
Copy link
Copy Markdown
Contributor Author

AlexAlves87 commented May 8, 2026

Hey @BunsDev apologies for the delay. I've now added the full test plan checkboxes and a screen recording as real behavior proof (see the updated PR body). CI is green. Let me know if anything else is needed.

@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 8, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 8, 2026
Copy link
Copy Markdown
Member

@BunsDev BunsDev left a comment

Choose a reason for hiding this comment

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

Approved. I rechecked the current head c59bb4f after the proof update.

What changed since the maintainer cleanup is limited to merging current main; the PR diff against current main is still scoped to CHANGELOG.md, ui/src/ui/app-native-bridge.ts, ui/src/ui/app-native-bridge.test.ts, and ui/src/ui/app.ts.

Verification reviewed:

  • Exact-head CI is green.
  • Real behavior proof is green after the PR body update.
  • The linked 47.9s MP4 proof is reachable and shows the WebView2 host draft injection, repeated dispatch without duplicate visible behavior, and regular-browser window.chrome?.webview returning undefined.
  • Prior maintainer local/Testbox proof remains applicable for the code path: focused bridge test, oxfmt, oxlint, and pnpm check:changed.

This is ready for maintainer landing once the merge instruction is given.

@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 8, 2026
@BunsDev BunsDev merged commit 41130a8 into openclaw:main May 8, 2026
92 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…9633)

Summary:
- Add the Control UI SPA-side WebView2 bridge for native Windows hosts.
- Route native `draft-text` messages through the existing chat draft path and send the `ready` handshake on bridge setup.
- Cover listener ordering, cleanup, malformed message handling, browser no-op behavior, and input-history reset.

Verification:
- Exact-head CI passed on c59bb4f.
- Real behavior proof passed with Windows/WebView2 screen recording evidence.
- Maintainer review approved.
rogerdigital pushed a commit to rogerdigital/openclaw that referenced this pull request May 9, 2026
…9633)

Summary:
- Add the Control UI SPA-side WebView2 bridge for native Windows hosts.
- Route native `draft-text` messages through the existing chat draft path and send the `ready` handshake on bridge setup.
- Cover listener ordering, cleanup, malformed message handling, browser no-op behavior, and input-history reset.

Verification:
- Exact-head CI passed on c59bb4f.
- Real behavior proof passed with Windows/WebView2 screen recording evidence.
- Maintainer review approved.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants