feat(macos): make OpenWorlds the native app shell - #135
Conversation
📝 WalkthroughWalkthroughAdds a bidirectional JS↔Swift native bridge and integrates it end-to-end: WebView bridge contract and injection, RootView viewer launch/state/handler, React frontend native-state and capability UI, a native settings panel, and server/test updates to include the new bridge asset. ChangesNative Bridge Implementation & Integration
Sequence Diagram(s)sequenceDiagram
participant ViewerJS as viewer/openworlds (JS)
participant OpenWorldsNative as window.OpenWorldsNative
participant WKWebView as WKWebView (nativeBridgeScript)
participant Coordinator as WebView.Coordinator
participant RootView as RootView.handleNativeRequest
participant ProcessService as processService
ViewerJS->>OpenWorldsNative: request(type, payload)
OpenWorldsNative->>WKWebView: forwards to window.ClawDnDNative.request(...)
WKWebView->>Coordinator: "clawdnd" postMessage
Coordinator->>RootView: parsed NativeBridgeRequest (await)
alt start viewer/provider
RootView->>ProcessService: startOpenWorlds() / startProviderSession(...)
ProcessService->>RootView: process started / port/url
RootView->>Coordinator: NativeBridgeReply(success)
else diagnostics/status
RootView->>RootView: build appStatus/diagnostics payload
RootView->>Coordinator: NativeBridgeReply(payload)
end
Coordinator->>WKWebView: evaluateJavaScript window.ClawDnDNative._reply(...)
WKWebView->>ViewerJS: reply JSON resolved
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift`:
- Around line 112-114: The current readiness check treats any HTTP status
200–499 as "ready", which incorrectly accepts 4xx client errors; update the HTTP
status check in the response handling (the HTTPURLResponse statusCode branch) to
only treat successful responses as ready (e.g., 200..<300), and explicitly
handle redirects (300..<400) or client errors (400..<500) according to desired
behavior—e.g., fail immediately on 4xx, and either follow/handle 3xx redirects
or treat them as not-ready; modify the branch that checks `if let http =
response as? HTTPURLResponse, (200..<500).contains(http.statusCode)` accordingly
to use the narrower ranges and explicit handling.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift`:
- Around line 101-103: dismantleNSView always calls
nsView.configuration.userContentController.removeScriptMessageHandler(forName:
"clawdnd") even though the handler is only added when nativeRequestHandler is
non-nil; update dismantleNSView to only remove the script message handler if the
Coordinator's nativeRequestHandler exists (e.g., check
coordinator.nativeRequestHandler != nil) or otherwise mirror the same condition
used when adding the handler so removal is conditional and consistent with the
add behavior.
- Around line 184-192: The send(_ reply: NativeBridgeReply) currently swallows
JSONSerialization failures; update it to capture and log serialization errors so
failed replies aren’t silent: replace the guard/try? with a do/catch that
attempts JSONSerialization.data(withJSONObject:), build the json String, and on
catch log the reply.dictionary and the caught error (e.g., via
NSLog/os_log/print) before returning; keep using
webView?.evaluateJavaScript("window.ClawDnDNative &&
window.ClawDnDNative._reply(\(json));") when serialization succeeds.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2f1c6030-6782-44e1-9c72-e3de820e6cc1
📒 Files selected for processing (15)
docs/OPENWORLDS_NATIVE_APP_ROADMAP.mdmacos/ClawDnDApp/Sources/ClawDnDApp/App/ClawDnDApp.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Models/LocalEndpoint.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swiftviewer/openworlds/SOURCE.mdviewer/openworlds/app.jsxviewer/openworlds/chrome.jsxviewer/openworlds/index.htmlviewer/openworlds/native-bridge.jsviewer/openworlds/screen-launcher.jsxviewer/openworlds/screen-settings.jsxviewer/openworlds/styles.cssviewer/server.pyviewer/tests/test_openworlds_static.py
💤 Files with no reviewable changes (1)
- viewer/openworlds/screen-launcher.jsx
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (2)
viewer/openworlds/chrome.jsx (1)
viewer/openworlds/app.jsx (1)
TitleBar(164-164)
viewer/openworlds/screen-settings.jsx (1)
viewer/openworlds/screen-character.jsx (1)
StatLine(445-457)
🪛 LanguageTool
docs/OPENWORLDS_NATIVE_APP_ROADMAP.md
[style] ~60-~60: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... app home with live/stale run state. 6. Sprint 5+: finish gameplay surfaces in impact ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
🪛 SwiftLint (0.63.2)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift
[Warning] 141-141: Classes should have an explicit deinit method
(required_deinit)
🔍 Remote MCP DeepWiki
The DeepWiki tools are indicating that this is a private repository, which I cannot access through the public endpoint. However, I can provide a comprehensive analysis based on the detailed AI-generated summaries you've already provided in the context.
Summary of Additional Relevant Context
Based on the AI-generated summaries provided, here are the key architectural and contractual details for reviewing this PR:
Native Bridge Contract
The new NativeBridgeRequest and NativeBridgeReply structs in WebView.swift define a request/response protocol:
- Request format:
{ type, requestId, payload }with basic validation - Reply format: Success/failure with optional error details, exported as JSON dictionary
- Supported request types (per
RootView.swifthandlers):appStatus— query app status and dependencies- Dependency/provider status queries
- Viewer control (
startOpenWorlds,stopOpenWorlds) - Provider session control (
startProviderSession,stopProviderSession) with payload parsing and runId generation - Diagnostics retrieval
- Dashboard navigation via
NSWorkspace
State-Authority Constraints
Per the roadmap summary, the native/app code is explicitly constrained from directly writing to:
snapshot.json- Play state
- QA state
- Inventory
- Quests
- XP
- Clocks
- Companion state
- Private lore
Campaign state write authority remains unchanged; the viewer exposes read models, and OpenWorlds posts player intent via existing routes.
Capability-Label Taxonomy
Four capability labels are defined (implemented in CapabilityBadge component):
- Wired (
emerald) — full native bridge integration available - Display-only (
royal) — read-only view, no native actions - Provider required (
brass) — feature requires active provider session - Unavailable (
crimson) — feature not available
Startup/Readiness Flow
RootView.swift implements a startup sequence:
refresh()— refresh dependencies and reload campaign storestartOpenWorlds()— launch local viewer viaprocessServicewaitForOpenWorlds()— poll local URL with fixed timeout until ready- Display
OpenWorldsLaunchOverlayduring startup or on errors - Load
WebViewonce URL is available
URL Routing Fix
viewer/server.py now redirects exact requests to /_OPENWORLDS_ROUTE → /_OPENWORLDS_ROUTE/<query> (with 302 + Cache-Control: no-store) to fix trailing-slash issues in WKWebView.
Settings Integration
The new "Native" settings section (NativeAppSection) in screen-settings.jsx:
- Derives bridge/app/viewer/provider/dependency data from
nativeState - Provides
nativeAction()helper with bridge availability checks and toast notifications - Displays bridge status, viewer status, provider status, and app state stats
- Includes action buttons for start/stop viewer/provider, copy diagnostics, debug dashboard
🔇 Additional comments (16)
viewer/server.py (1)
2852-2857: LGTM!Also applies to: 3014-3017
viewer/tests/test_openworlds_static.py (1)
50-53: LGTM!Also applies to: 53-59, 65-71, 81-81
docs/OPENWORLDS_NATIVE_APP_ROADMAP.md (1)
93-105: ⚡ Quick winNative bridge request names match implementation (no startOpenWorlds/stopOpenWorlds drift)
docs/OPENWORLDS_NATIVE_APP_ROADMAP.md’sstartViewer/stopViewerlist matches the actual bridgerequest.typehandling inmacos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swiftand the JS callers inviewer/openworlds/screen-settings.jsx. ThestartOpenWorldshits inRootView.swiftare internal Swift helper methods (not bridge request-type strings), so there’s no contract-name mismatch to align.macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift (1)
5-54: LGTM!
NativeBridgeRequestvalidation is sound (type required, requestId fallback, payload defaults)NativeBridgeReplyfactory methods and dictionary export are clean- JS bridge uses IIFE to avoid globals pollution, handles missing
crypto.randomUUID, and dispatches both callback resolution and CustomEventAlso applies to: 105-139
viewer/openworlds/native-bridge.js (1)
1-15: LGTM!Clean wrapper providing bridge availability check and error handling when running outside the native app context.
viewer/openworlds/index.html (1)
20-20: LGTM!viewer/openworlds/SOURCE.md (1)
24-24: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift (1)
21-67: LGTM!
- Startup state machine handles cancellation correctly
launchTask?.cancel()inonDisappearprevents orphaned tasks- Native request handler covers expected request types with appropriate error propagation
- Payload builders properly expose endpoint/provider/dependency data
Also applies to: 69-99, 124-165, 167-244, 246-314
macos/ClawDnDApp/Sources/ClawDnDApp/Models/LocalEndpoint.swift (1)
25-29: LGTM!Properly ensures trailing slash for WKWebView compatibility using
URLComponents, with safe fallback.macos/ClawDnDApp/Sources/ClawDnDApp/App/ClawDnDApp.swift (1)
7-7: LGTM!Debug Control Center window properly wired with distinct shortcut (Cmd+Option+D vs Cmd+Shift+D for diagnostics).
Also applies to: 18-31
viewer/openworlds/app.jsx (2)
13-19: LGTM!
nativeStateinitialization correctly checks bridge availability synchronouslyrefreshNativeproperly handles both success and failure cases with appropriate state updates- Effect cleanup correctly removes event listener and clears interval
capabilityForScreenprovides clear per-screen capability classificationAlso applies to: 74-107, 252-265
167-168: LGTM!Props correctly threaded to
TitleBarandScreenRouter, with settings screen receiving native state for the new settings section.Also applies to: 177-186, 267-287
viewer/openworlds/chrome.jsx (1)
356-366: LGTM!Also applies to: 368-368, 381-381, 391-391
viewer/openworlds/styles.css (1)
235-236: LGTM!Also applies to: 238-263
viewer/openworlds/screen-settings.jsx (2)
3-4: LGTM!Also applies to: 12-12, 58-60, 220-274, 279-315
275-278: ⚡ Quick winConfirm native action names match the Swift bridge (no change needed).
nativeAction("startViewer")/nativeAction("stopViewer")inviewer/openworlds/screen-settings.jsxmap tocase "startViewer"/case "stopViewer"inmacos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift.nativeAction("stopProvider")maps tocase "stopProvider"inRootView.swift.nativeAction("startProviderSession")(line 246) maps tocase "startProviderSession"inRootView.swift.- The proposed renaming to
startOpenWorlds/stopOpenWorlds/stopProviderSessionis not consistent with the dispatcher cases found in the Swift sources.Proposed fix
- <BrassButton size="sm" onClick={() => nativeAction("startViewer")}>Start Viewer</BrassButton> - <BrassButton size="sm" tone="ghost" onClick={() => nativeAction("stopViewer")}>Stop Viewer</BrassButton> + <BrassButton size="sm" onClick={() => nativeAction("startOpenWorlds")}>Start Viewer</BrassButton> + <BrassButton size="sm" tone="ghost" onClick={() => nativeAction("stopOpenWorlds")}>Stop Viewer</BrassButton> <BrassButton size="sm" onClick={startProvider}>Start Provider</BrassButton> - <BrassButton size="sm" tone="ghost" onClick={() => nativeAction("stopProvider")}>Stop Provider</BrassButton> + <BrassButton size="sm" tone="ghost" onClick={() => nativeAction("stopProviderSession")}>Stop Provider</BrassButton>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift`:
- Line 67: RootView currently renders a full-screen WebView that intercepts hits
while OpenWorldsWindowChrome.configure removed the native titlebar/buttons and
set window.isMovableByWindowBackground = true, so restore native affordances or
add a drag region: either (A) stop removing .titled and window.toolbar in
OpenWorldsWindowChrome.configure (or defer those mutations until a custom chrome
view is attached) so standardWindowButton(.close/.miniaturize/.zoom) remain
available, or (B) add a dedicated drag region and native buttons in RootView by
placing a non-opaque top-area view (e.g., DragRegionView) above the WebView that
forwards window drag events and hosts NSWindow.standardWindowButton instances
and their actions; ensure the WebView does not cover that region (don’t use
ignoresSafeArea for the top control area) and preserve
window.isMovableByWindowBackground only if the drag region is present.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift`:
- Around line 76-79: The WKScriptMessageHandler "clawdnd" is currently callable
from any page when nativeRequestHandler != nil; update
Coordinator.userContentController(_:didReceive:) to first verify webView?.url
matches the trusted local OpenWorlds origin (scheme/host and optional path) and
ignore any messages from other origins, and implement
Coordinator.webView(_:decidePolicyFor:decisionHandler:) to allow only top-level
navigations whose URL matches that same allowlist (call decisionHandler(.cancel)
for others); additionally, when navigating away from the trusted origin consider
removing or temporarily disabling the "clawdnd" handler (or setting
coordinator.hasNativeMessageHandler = false) until a trusted page loads to
prevent message exposure.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 29b1f748-2678-4a7e-9769-94338b13cedb
📒 Files selected for processing (3)
docs/OPENWORLDS_NATIVE_APP_ROADMAP.mdmacos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift (2)
makeNSView(74-74)updateNSView(90-90)
🪛 SwiftLint (0.63.2)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift
[Warning] 343-343: Classes should have an explicit deinit method
(required_deinit)
🔇 Additional comments (3)
docs/OPENWORLDS_NATIVE_APP_ROADMAP.md (1)
56-57: LGTM!Also applies to: 86-96
macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift (1)
113-130: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift (1)
102-105: LGTM!Also applies to: 188-203
| .onDisappear { | ||
| launchTask?.cancel() | ||
| } | ||
| .background(OpenWorldsWindowChrome()) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the file
ls -la macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift
# Show the relevant region around the reported lines
sed -n '1,140p' macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift | nl -ba | sed -n '1,160p'
# Show around “Also applies to: 349-369” (likely different function/struct in same file)
sed -n '300,430p' macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift | nl -ba
# Search for the specific window mutations mentioned in the review
rg -n "styleMask\.remove\(\.titled\)|isMovableByWindowBackground|standardWindowButton|titleVisibility|titlebarAppearsTransparent|fullSizeContentView|resizable|toolbar\s*=" macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift
# Search the repo for other configure(_ window:) implementations affecting titlebar/movable behavior
rg -n "static func configure\\(_ window: NSWindow\\?\\)" -S macos/ClawDnDApp/Sources/ClawDnDApp | headRepository: 100yenadmin/ClawDnD
Length of output: 231
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift"
ls -la "$FILE"
# Print with line numbers using awk (no nl in sandbox)
awk 'NR>=1 && NR<=180 {printf "%6d: %s\n", NR, $0}' "$FILE"
awk 'NR>=300 && NR<=430 {printf "%6d: %s\n", NR, $0}' "$FILE"
# Search for the specific window mutations mentioned
rg -n "styleMask\.remove\(\.titled\)|isMovableByWindowBackground|standardWindowButton|titleVisibility|titlebarAppearsTransparent|fullSizeContentView|resizable|toolbar\s*=" "$FILE"
# Find other occurrences in repo of this exact configure signature
rg -n "static func configure\(_ window: NSWindow\?\)" -S macos/ClawDnDApp/Sources/ClawDnDApp | head -n 50Repository: 100yenadmin/ClawDnD
Length of output: 14934
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the WebView implementation
rg -n "struct WebView|class WebView|WebView\(" -S macos/ClawDnDApp/Sources/ClawDnDApp | head -n 50
# Locate WKWebView usage
rg -n "WKWebView" -S macos/ClawDnDApp/Sources/ClawDnDApp | head -n 50
# Show the likely WebView file (best guess from search results)
# We'll dump small windows around matches
rg -n "WKWebView" -S macos/ClawDnDApp/Sources/ClawDnDApp --context 5 | head -n 120Repository: 100yenadmin/ClawDnD
Length of output: 8355
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift"
ls -la "$FILE"
wc -l "$FILE"
awk 'NR>=1 && NR<=240 {printf "%6d: %s\n", NR, $0}' "$FILE" | head -n 260Repository: 100yenadmin/ClawDnD
Length of output: 10883
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for any custom drag region / titlebar / window controls
rg -n "drag|draggable|titlebar|titlebarAccessory|move|window\.performClose|miniaturize|zoomButton|standardWindowButton|isMovableByWindowBackground" macos/ClawDnDApp/Sources/ClawDnDApp | head -n 200
# Specifically look for window close/minimize/zoom actions implemented in SwiftUI/AppKit
rg -n "performClose|miniaturize|zoom|setFrame|setFrameOrigin|window\\.close\\(|window\\.miniaturize\\(|window\\.zoom\\(" macos/ClawDnDApp/Sources/ClawDnDApp | head -n 200
# Look for NSWindow / styleMask changes besides RootView.swift
rg -n "styleMask\." macos/ClawDnDApp/Sources/ClawDnDApp | head -n 200
# Check if any view sets "allowsHitTesting(false)" etc around WebView or overlays
rg -n "allowsHitTesting|contentShape|simultaneousGesture|highPriorityGesture|gesture|onTapGesture" macos/ClawDnDApp/Sources/ClawDnDApp/Views | head -n 200Repository: 100yenadmin/ClawDnD
Length of output: 1864
Restore native titlebar move/close/minimize/zoom affordances (or add a dedicated drag region) in RootView
OpenWorldsWindowChrome.configureremoveswindow.styleMask.remove(.titled), hides the standard close/miniaturize/zoom buttons, and setswindow.toolbar = nil, leaving onlywindow.isMovableByWindowBackground = truefor window movement.RootViewrenders a full-screenWebView(WKWebView) viaZStack { ... WebView(...).ignoresSafeArea() }, so the web surface will intercept hit-testing; there’s no dedicated drag region/window-control surface implemented alongside it in this code.
Either defer these titlebar/button mutations until the custom frame exists, or add an explicit drag region plus native close/minimize/zoom actions.
Possible minimal rollback
static func configure(_ window: NSWindow?) {
guard let window else { return }
DispatchQueue.main.async {
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
window.styleMask.insert(.fullSizeContentView)
- window.styleMask.remove(.titled)
window.styleMask.insert(.resizable)
window.toolbar = nil
window.backgroundColor = .black
window.isOpaque = true
- window.isMovableByWindowBackground = true
-
- [
- NSWindow.ButtonType.closeButton,
- .miniaturizeButton,
- .zoomButton
- ].forEach { button in
- window.standardWindowButton(button)?.isHidden = true
- }
}
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift` at line 67,
RootView currently renders a full-screen WebView that intercepts hits while
OpenWorldsWindowChrome.configure removed the native titlebar/buttons and set
window.isMovableByWindowBackground = true, so restore native affordances or add
a drag region: either (A) stop removing .titled and window.toolbar in
OpenWorldsWindowChrome.configure (or defer those mutations until a custom chrome
view is attached) so standardWindowButton(.close/.miniaturize/.zoom) remain
available, or (B) add a dedicated drag region and native buttons in RootView by
placing a non-opaque top-area view (e.g., DragRegionView) above the WebView that
forwards window drag events and hosts NSWindow.standardWindowButton instances
and their actions; ensure the WebView does not cover that region (don’t use
ignoresSafeArea for the top control area) and preserve
window.isMovableByWindowBackground only if the drag region is present.
| if nativeRequestHandler != nil { | ||
| configuration.userContentController.addUserScript(Self.nativeBridgeScript) | ||
| configuration.userContentController.add(context.coordinator, name: "clawdnd") | ||
| context.coordinator.hasNativeMessageHandler = true |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift"
echo "== File = $FILE =="
wc -l "$FILE" || true
echo
echo "== Lines 60-140 =="
nl -ba "$FILE" | sed -n '60,140p'
echo
echo "== Lines 140-220 =="
nl -ba "$FILE" | sed -n '140,220p'
echo
echo "== Search: nativeBridgeScript / didReceive / forMainFrameOnly / navigation delegate =="
rg -n "nativeBridgeScript|didReceive\\(|forMainFrameOnly|WKNavigationDelegate|decidePolicyFor|navigationAction|navigationResponse|targetFrame|trusted|origin|host|URL|load\\(" "$FILE" -S || trueRepository: 100yenadmin/ClawDnD
Length of output: 274
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift"
echo "== File = $FILE =="
wc -l "$FILE" || true
echo
echo "== Lines 60-140 (no nl available) =="
sed -n '60,140p' "$FILE"
echo
echo "== Lines 140-220 =="
sed -n '140,220p' "$FILE"
echo
echo "== Search hits in file =="
rg -n "nativeBridgeScript|didReceive\\(|forMainFrameOnly|WKNavigationDelegate|decidePolicyFor|navigationAction|navigationResponse|targetFrame|trusted|origin|host|URL|load\\(" "$FILE" -S || true
echo
echo "== Show WebViewCoordinator didReceive handler if present =="
rg -n "class .*Coordinator|struct .*Coordinator|Coordinator|userContentController\\(" "$FILE" -S || trueRepository: 100yenadmin/ClawDnD
Length of output: 7836
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift"
echo "== Navigation policy methods in this file =="
rg -n "decidePolicyFor|navigationAction|navigationResponse|createWebView|didStartProvisionalNavigation|didCommit|didReceiveServerRedirectForProvisionalNavigation" "$FILE" -S || true
echo
echo "== Full Coordinator method scan for URL/origin checks =="
rg -n "url\\b|origin\\b|host\\b|scheme\\b|trusted|allow(list)?|OpenWorlds|localhost|127\\.0\\.0\\.1|file://" "$FILE" -S || true
echo
echo "== Read remainder around where url loads (lines ~60-120 already; now ~1-120 and ~220-234) =="
sed -n '1,130p' "$FILE"
echo
sed -n '130,234p' "$FILE"Repository: 100yenadmin/ClawDnD
Length of output: 9345
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "WebView\\(" macos/ClawDnDApp/Sources -S
echo
rg -n "init\\(\\s*url:\\s*URL\\?" macos/ClawDnDApp/Sources -S || true
echo
rg -n "nativeRequestHandler|NativeRequestHandler" macos/ClawDnDApp/Sources -S
echo
rg -n "clawdnd\"|webkit\\.messageHandlers\\.clawdnd|clawdnd:native" -S .Repository: 100yenadmin/ClawDnD
Length of output: 3158
🏁 Script executed:
#!/bin/bash
set -euo pipefail
base="macos/ClawDnDApp/Sources/ClawDnDApp/Views"
for f in RootView.swift PlayView.swift MonitorView.swift; do
echo "===== $base/$f (head) ====="
sed -n '1,140p' "$base/$f"
echo
done
echo "===== viewer/openworlds/app.jsx (around native-ready/listeners) ====="
sed -n '1,160p' viewer/openworlds/app.jsxRepository: 100yenadmin/ClawDnD
Length of output: 17780
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift"
rg -n "handleNativeRequest\\(|startProviderSession|stopViewer|copyDiagnostics|diagnostics|provider|viewer|NativeBridgeRequest|switch\\s*request\\.type|request\\.type" "$FILE" -S
echo
echo "== Show handleNativeRequest function region =="
# Print a window around the definition (best-effort with sed line ranges)
# First find line number
LN=$(rg -n "private func handleNativeRequest\\(" "$FILE" -n | sed -E 's/[^:]*:([0-9]+).*/\1/' | head -n1)
echo "handleNativeRequest at line $LN"
START=$((LN-30))
END=$((LN+200))
sed -n "${START},${END}p" "$FILE"Repository: 100yenadmin/ClawDnD
Length of output: 2663
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift"
# Get line number of handleNativeRequest
LN=$(rg -n "private func handleNativeRequest\\(" "$FILE" | head -n1 | awk -F: '{print $1}')
echo "handleNativeRequest at line $LN"
START=$((LN-40))
END=$((LN+220))
sed -n "${START},${END}p" "$FILE"Repository: 100yenadmin/ClawDnD
Length of output: 10323
Restrict the clawdnd native bridge to the trusted local OpenWorlds origin
- When
nativeRequestHandler != nil,WebView.swiftregisters aWKScriptMessageHandlernamedclawdndandCoordinator.userContentController(_:didReceive:)dispatchesrequest.typedirectly to privileged actions (startViewer,stopViewer,startProviderSession,stopProvider,diagnostics/copyDiagnostics, andopenFallbackDashboard). - There’s no navigation-policy/origin allowlist: the
WKNavigationDelegatemethods only report load failures, anduserContentController(_:didReceive:)doesn’t checkwebView.url/origin before executing the request. - Although
nativeBridgeScriptis injected withforMainFrameOnly: true, the message handler itself is callable by any JavaScript in the loaded page once the handler is registered—so navigating the main frame away from the local OpenWorlds surface can expose these native actions to untrusted content. - Add an allowlist gate:
- In
Coordinator.userContentController(_:didReceive:), only handle messages whenwebView?.urlmatches the expected local OpenWorlds scheme/host (and optionally path). - In
CoordinatorimplementwebView(_:decidePolicyFor:decisionHandler:)to cancel any top-level navigation that doesn’t match that allowlist (and consider removing/temporarily disabling the handler when navigating away).
- In
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift` around lines 76 -
79, The WKScriptMessageHandler "clawdnd" is currently callable from any page
when nativeRequestHandler != nil; update
Coordinator.userContentController(_:didReceive:) to first verify webView?.url
matches the trusted local OpenWorlds origin (scheme/host and optional path) and
ignore any messages from other origins, and implement
Coordinator.webView(_:decidePolicyFor:decisionHandler:) to allow only top-level
navigations whose URL matches that same allowlist (call decisionHandler(.cancel)
for others); additionally, when navigating away from the trusted origin consider
removing or temporarily disabling the "clawdnd" handler (or setting
coordinator.hasNativeMessageHandler = false) until a trusted page loads to
prevent message exposure.
Summary
This correction PR makes OpenWorlds the visible native macOS app experience
instead of embedding it inside the old SwiftUI control shell.
The Swift/AppKit layer remains important, but only as the native supervisor:
launching the viewer, tracking dependencies/providers/logs, handling local
diagnostics, and exposing a narrow WKWebView bridge. The product UI now opens
directly into the OpenWorlds window frame, nav rail, parchment stage, and
surface routing.
Refs #82
Refs #113
Refs #114
Refs #131
Refs #132
Refs #133
Refs #134
Refs #136
What changed
RootViewwith a full-windowOpenWorldsHostView.viewer/server.pyon app launch and waits for/openworlds/readiness before loading WKWebView.
Center window (
Option+Command+D) for temporary recovery/debugging./openworlds/.viewer/server.pyredirects/openworldsto/openworlds/.viewer/tests/test_openworlds_static.py.window.ClawDnDNative.request(type, payload)through aWKScriptMessagebridge.
Wired,Display-only,Provider required,Unavailable) so prototype screens do not pretend to be fully backed.designed OpenWorlds frame is the only visible app chrome in normal play.
/move; no directcampaign-state writes are added.
docs/OPENWORLDS_NATIVE_APP_ROADMAP.md, including the Sparkle updatelane ([macos][release] Add Sparkle update channel for native app #134) and custom window chrome lane ([macos][openworlds] Make OpenWorlds own native window chrome and controls #136).
Architecture notes
State authority is unchanged:
snapshot.json,play-state,qa/state,inventory, quests, XP, clocks, companion state, or private lore directly.
Visual validation
Local screenshots captured from the running
.app:/Volumes/LEXAR/Codex/clawdnd-openworlds-macos-roadmap-2026-05-26/app-smoke.png/Volumes/LEXAR/Codex/clawdnd-openworlds-macos-roadmap-2026-05-26/app-settings-smoke.png/Volumes/LEXAR/Codex/clawdnd-openworlds-macos-roadmap-2026-05-26/app-window-chrome-smoke-4.pngThe visible app is
ClawDnDAppcontaining OpenWorlds. Normal launch no longershows the old SwiftUI sidebar/control bar, and the play surface is not handed
off to Safari.
Validation
Ran locally from
/Volumes/LEXAR/repos/ClawDnD-openworlds-native-app-correction:Also smoked:
curl -fsS http://127.0.0.1:8765/openworlds/ curl -fsS http://127.0.0.1:8765/openworlds/config.json | python3 -m json.toolNo story/DM content changes, no narrative QA runs, and no private runtime state
is included.
Rollback
Revert this PR to restore the prior SwiftUI-first macOS shell while keeping the
existing viewer routes available on
main. The OpenWorlds static surface andviewer read models remain separate from campaign state.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests