Right now WebChatWindow and CanvasWindow both load gateway content in WebView2, but there's no formal communication channel between the native C# side and the embedded SPA. The two halves run disconnected, which shows up in a few places already:
| Surface |
Current situation |
WebChatWindow |
No bridge at all. Native code has no way to signal UI state to the SPA (recording indicator, draft text, activity spinner) |
CanvasWindow |
One-way native→web channel via ExecuteScriptAsync with 11 heuristic fallbacks. No web→native channel |
| Voice PR #120 (NichUK) |
Works around the gap with DOM scraping (querySelector, MutationObserver, descriptor hacks). The author describes it as fragile |
| Screen record PR #159 |
No way to show a recording indicator in the SPA while the app is capturing |
QuickSendDialog |
Can send messages over WebSocket but can't coordinate UI state with the embedded chat |
The fix
WebView2 ships a proper bidirectional bridge that doesn't touch the DOM at all:
Native → SPA
// Inject bootstrap before any page scripts run
await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(BridgeInitScript);
// Send a message at any point
webView.CoreWebView2.PostWebMessageAsJson(JsonSerializer.Serialize(msg));
SPA → Native
webView.CoreWebView2.WebMessageReceived += (_, e) => {
var msg = JsonSerializer.Deserialize<BridgeMessage>(e.WebMessageAsJson);
HandleBridgeMessage(msg);
};
Gateway SPA side
// Receive from native
window.chrome.webview.addEventListener('message', e => {
const msg = e.data; // { type, payload }
// dispatch by type
});
// Send to native
window.chrome.webview.postMessage({ type: 'ready' });
Minimal message contract to start
{ "type": "recording-start", "payload": {} }
{ "type": "recording-stop", "payload": {} }
{ "type": "voice-start", "payload": {} }
{ "type": "voice-stop", "payload": {} }
{ "type": "draft-text", "payload": { "text": "..." } }
{ "type": "ready", "payload": {} }
Scope
This touches two repos:
- This repo — wire the bridge in
WebChatWindow.xaml.cs and CanvasWindow.xaml.cs
- Gateway — expose the
window.chrome.webview listener in the SPA
The existing WebSocket transport (chat.send) stays as-is — it handles chat messages. The bridge is purely for UI state coordination.
Happy to put together a PR for the Windows side once there's alignment on the gateway contract.
Related
Right now
WebChatWindowandCanvasWindowboth load gateway content in WebView2, but there's no formal communication channel between the native C# side and the embedded SPA. The two halves run disconnected, which shows up in a few places already:WebChatWindowCanvasWindowExecuteScriptAsyncwith 11 heuristic fallbacks. No web→native channelquerySelector,MutationObserver, descriptor hacks). The author describes it as fragileQuickSendDialogThe fix
WebView2 ships a proper bidirectional bridge that doesn't touch the DOM at all:
Native → SPA
SPA → Native
Gateway SPA side
Minimal message contract to start
{ "type": "recording-start", "payload": {} } { "type": "recording-stop", "payload": {} } { "type": "voice-start", "payload": {} } { "type": "voice-stop", "payload": {} } { "type": "draft-text", "payload": { "text": "..." } } { "type": "ready", "payload": {} }Scope
This touches two repos:
WebChatWindow.xaml.csandCanvasWindow.xaml.cswindow.chrome.webviewlistener in the SPAThe existing WebSocket transport (
chat.send) stays as-is — it handles chat messages. The bridge is purely for UI state coordination.Happy to put together a PR for the Windows side once there's alignment on the gateway contract.
Related