feat(ui): add SPA-side support for WebView2 native bridge#69633
feat(ui): add SPA-side support for WebView2 native bridge#69633BunsDev merged 11 commits intoopenclaw:mainfrom
Conversation
cb0f814 to
54b3d31
Compare
|
Codex review: needs maintainer review before merge. Summary 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 Next step before merge Security Review detailsBest 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:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 4726755f6a16. |
7be00dd to
2aefa11
Compare
Greptile SummaryThis PR wires a WebView2 native bridge into the SPA lifecycle, adding The surface is correctly scoped to Confidence Score: 4/5Safe 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 AIThis 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 |
6bcfa44 to
aabd9ab
Compare
|
I dug into the CI failures and they don't come from the bridge itself. Both visible errors ( The root cause was a stale fixture in I've rebased the branch onto current |
|
@BunsDev sorry for the noise. The root cause was on my side: Pushing a fix now that adds the suppress in |
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>
b171ab0 to
0224a82
Compare
|
Maintainer update: I pushed Verification:
Remaining blocker: @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
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. |
BunsDev
left a comment
There was a problem hiding this comment.
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 proofis 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?.webviewreturningundefined. - 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.
…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.
…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.
Summary
Wires the WebView2 bridge into the SPA lifecycle with a minimal, parity-aligned surface.
app-native-bridge.tsreducesNativeBridgeMessagetodraft-text(inbound) andready(outbound handshake). Removesrecording-start/stopandvoice-start/stop, since there is no corresponding SPA handler or UI surface today, and recording follows the parity decision inopenclaw-windows-node#159.NativeBridgeHostnow requires onlyhandleChatDraftChange(next).app.tsimportsinitNativeBridge, calls it inconnectedCallback, and cleans it up indisconnectedCallback.draft-textgoes throughhandleChatDraftChangeso 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
{"type":"draft-text","payload":{"text":"hello"}}then chat input updates and input-history navigation resetsinitNativeBridgeno-ops without errorsReal behavior proof
draft-textmessages 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.127.0.0.1:18789, branchfeat/webview2-bridge-spaon top offeat/webview2-bridgeinopenclaw-windows-node.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); verifywindow.chrome?.webviewreturnsundefined."bridge test"appeared in the chat input after thedispatchEventcall. Rapid re-dispatch (00:28-00:32) produced no duplication.window.chrome?.webviewreturnedundefinedin the browser dashboard at 00:43.Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com