Skip to content

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

Open
AlexAlves87 wants to merge 10 commits intoopenclaw:mainfrom
AlexAlves87:feat/webview2-bridge-spa
Open

feat(ui): add SPA-side support for WebView2 native bridge#69633
AlexAlves87 wants to merge 10 commits intoopenclaw:mainfrom
AlexAlves87:feat/webview2-bridge-spa

Conversation

@AlexAlves87
Copy link
Copy Markdown

@AlexAlves87 AlexAlves87 commented Apr 21, 2026

Summary

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

  • ^Gpp-native-bridge.ts\ — reduces \NativeBridgeMessage\ to \draft-text\ (inbound) and \eady\ (outbound handshake). Removes \ecording-start/stop\ and \oice-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).
  • ^Gpp.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}}\ → chat input updates and input-history navigation resets
  • Disconnect/reconnect component → no listener leak
  • Outside WebView2 (regular browser) → \initNativeBridge\ no-ops without errors

Real behavior proof

Environment: OpenClaw Windows Tray (WinUI 3, WebView2 host), local gateway at 127.0.0.1:18789, branch feat/webview2-bridge-spa.

Recording: Grabacion.de.pantalla.2026-05-08.160245.mp4

Gate Evidence in the video
draft-text injection At 00:13, the DevTools console of the widget executes window.chrome?.webview?.dispatchEvent(new MessageEvent('message',{data:{type:'draft-text',payload:{text:'bridge test'}}})) returning true. At 00:16, reopening the widget shows "bridge test" injected into the input field.
cleanup / reconnect The widget is closed and reopened multiple times (00:15→00:16, 00:27→00:33, 00:42→reopen). The draft text "bridge test" persists across every cycle. Additionally, rapid-fire spam of the same event (00:28–00:32) proves idempotency — no duplication or corruption on reconnect.
regular-browser no-op At 00:43, the main dashboard browser console evaluates window.chrome?.webview and returns undefined, proving the bridge API is absent in a standard browser and the code paths safely no-op.

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 real behavior proof before merge.

Summary
Adds a Control UI WebView2 bridge module, wires it into the OpenClawApp lifecycle, covers the draft-text/ready behavior with UI tests, and records the change in the changelog.

Reproducibility: not applicable. this is a feature PR, not a bug report. The JS-side behavior has high-confidence source and test coverage, but the native WebView2 host smoke path still needs real proof.

Real behavior proof
Needs real behavior proof before merge: The PR body lacks after-fix real WebView2 host evidence; tests, CI, and maintainer verification are supplemental but do not satisfy the external contributor proof gate. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, ask a maintainer to comment @clawsweeper re-review.

Next step before merge
Contributor action is required because automation cannot supply the external author's real WebView2 setup proof.

Security
Cleared: No concrete security or supply-chain concern found; the diff only adds Control UI TypeScript/tests/changelog and validates a narrow native message shape.

Review details

Best possible solution:

Keep the narrow bridge, but merge only after the contributor adds redacted real WebView2 host proof for draft injection, reconnect cleanup, and regular-browser no-op behavior.

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

Not applicable: this is a feature PR, not a bug report. The JS-side behavior has high-confidence source and test coverage, but the native WebView2 host smoke path still needs real proof.

Is this the best way to solve the issue?

Yes for the code direction: the draft-text/ready-only SPA bridge is the narrow maintainable counterpart to the native-side contract. It is not merge-ready until the external real behavior proof gate is satisfied.

What I checked:

  • current_main_has_no_spa_bridge: Current main has no existing SPA bridge implementation; targeted searches for app-native-bridge, initNativeBridge, NativeBridge, chrome.webview, WebView2, draft-text, and sendToNative under ui/src/ui, src, and docs returned no hits. (695d4ccd1b3d)
  • bridge_scope_is_narrow: The PR head defines a narrow bridge contract with inbound draft-text and outbound ready messages, validates message shape before applying draft text, and registers the listener before sending the ready handshake. (ui/src/ui/app-native-bridge.ts:7, cd7619b22eaf)
  • lifecycle_wiring_and_cleanup: OpenClawApp imports initNativeBridge, stores the cleanup callback, initializes it from connectedCallback, and calls it from disconnectedCallback. (ui/src/ui/app.ts:41, cd7619b22eaf)
  • tests_cover_js_side_behavior: The added tests cover WebView2 detection, ready handshake ordering, no-op behavior outside WebView2, valid and malformed draft-text messages, listener cleanup, and input-history reset behavior. (ui/src/ui/app-native-bridge.test.ts:75, cd7619b22eaf)
  • changelog_entry_present: The PR head includes an Unreleased changelog entry for the Control UI/Windows SPA-side WebView2 bridge. (CHANGELOG.md:15, cd7619b22eaf)
  • real_behavior_proof_missing: The PR body still has no Real behavior proof section, the PR carries triage: needs-real-behavior-proof, and the maintainer comment explicitly says real WebView2 host proof remains the blocker. (CONTRIBUTING.md:109, 695d4ccd1b3d)

Likely related people:

  • BunsDev: Feature history shows BunsDev introduced relevant chat/input-history infrastructure and recently maintained app.ts/app-chat behavior; BunsDev also rebased this PR and added the changelog entry. (role: likely Control UI owner and recent maintainer; confidence: high; commits: c5ea6134d041, 37aebf612b83, b1c515270eb5; files: ui/src/ui/app.ts, ui/src/ui/app-chat.ts, ui/src/ui/chat/input-history.ts)
  • Ivocin: Recent merged work hardened WebChat input-history behavior across app.ts, app-chat.ts, and chat/input-history.ts, which is the path used by native draft injection. (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 browser realtime, chat submit behavior, pending model switch handling, and app lifecycle surfaces near this bridge wiring. (role: adjacent Control UI chat and lifecycle maintainer; confidence: medium; commits: 04066d246abc, f6b2ba4a10af, 6785633d137c; files: ui/src/ui/app.ts, ui/src/ui/app-chat.ts, ui/src/ui/app-lifecycle.ts)

Remaining risk / open question:

  • No after-fix proof from a real OpenClaw/WebView2 host has been provided yet, so native-host behavior remains unverified outside mocks and CI.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 695d4ccd1b3d.

@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
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
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:

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 size: M triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants