Skip to content

feat(macos): make OpenWorlds the native app shell - #135

Merged
100yenadmin merged 2 commits into
mainfrom
macos/openworlds-native-app-correction
May 26, 2026
Merged

feat(macos): make OpenWorlds the native app shell#135
100yenadmin merged 2 commits into
mainfrom
macos/openworlds-native-app-correction

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented May 26, 2026

Copy link
Copy Markdown
Member

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

  • Replaced the normal macOS RootView with a full-window OpenWorldsHostView.
  • Auto-starts viewer/server.py on app launch and waits for /openworlds/
    readiness before loading WKWebView.
  • Moves the old SwiftUI sidebar/status/control surface behind a Debug Control
    Center window (Option+Command+D) for temporary recovery/debugging.
  • Fixes the WKWebView trailing-slash failure:
    • Swift loads /openworlds/.
    • viewer/server.py redirects /openworlds to /openworlds/.
    • Adds a regression test in viewer/tests/test_openworlds_static.py.
  • Adds window.ClawDnDNative.request(type, payload) through a WKScriptMessage
    bridge.
  • Adds an OpenWorlds native-bridge helper and bridge-aware Settings surface.
  • Adds capability badges (Wired, Display-only, Provider required,
    Unavailable) so prototype screens do not pretend to be fully backed.
  • Hides the native titlebar/traffic lights in the main OpenWorlds host so the
    designed OpenWorlds frame is the only visible app chrome in normal play.
  • Keeps browser/game intent on existing viewer routes and /move; no direct
    campaign-state writes are added.
  • Adds docs/OPENWORLDS_NATIVE_APP_ROADMAP.md, including the Sparkle update
    lane ([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:

  • Engine and player move paths remain the only campaign-state writers.
  • Viewer exposes browser-safe read models.
  • OpenWorlds reads viewer APIs and posts player intent through existing routes.
  • The native bridge starts/stops local processes and returns diagnostics/status.
  • No Swift/OpenWorlds code writes 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.png

The visible app is ClawDnDApp containing OpenWorlds. Normal launch no longer
shows 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:

python3 -m unittest viewer.tests.test_openworlds_static -q
python3 -m py_compile viewer/server.py
swift build --package-path macos/ClawDnDApp
./script/build_and_run.sh --verify
python3 scripts/license_check.py
git diff --check

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.tool

No 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 and
viewer read models remain separate from campaign state.

Summary by CodeRabbit

  • New Features

    • Debug Control Center window (Command‑Option‑D)
    • Web ↔ native bridge with request/reply plumbing and OpenWorldsNative helper
    • Native settings section with bridge/app/viewer/provider controls and actions
    • Capability badges showing availability and source; UI styles for badges
  • Bug Fixes

    • Fixed URL path construction for app endpoints
    • Corrected screen navigation behavior
  • Documentation

    • Added native app implementation roadmap
  • Tests

    • Static asset tests updated to include native bridge script

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds 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.

Changes

Native Bridge Implementation & Integration

Layer / File(s) Summary
Native bridge contract and protocol
macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift, viewer/openworlds/native-bridge.js, viewer/openworlds/SOURCE.md, viewer/openworlds/index.html
Adds NativeBridgeRequest/NativeBridgeReply, injects nativeBridgeScript into WKWebView, registers "clawdnd" message handler, implements Coordinator → async handler dispatch and reply eval, and exposes window.OpenWorldsNative wrapper in the viewer bundle.
App launcher and native request handler
macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift, macos/ClawDnDApp/Sources/ClawDnDApp/App/ClawDnDApp.swift, macos/ClawDnDApp/Sources/ClawDnDApp/Models/LocalEndpoint.swift
Reworks RootView to control processService-driven viewer start, readiness polling, and error overlay; implements handleNativeRequest paths (appStatus, start/stop viewer/provider, diagnostics, dashboard), adds Debug Control Center WindowGroup and openWindow wiring, and makes OpenWorlds URL construction robust via URLComponents.
Frontend native state and capability metadata
viewer/openworlds/app.jsx
Adds nativeState, refreshNative polling/trigger logic (on mount and clawdnd:native-ready) and passes nativeState/refreshNative into ScreenRouter/ScreenSettings; introduces capabilityForScreen(screen, nativeState).
Capability badge and title bar UI
viewer/openworlds/chrome.jsx, viewer/openworlds/styles.css
New CapabilityBadge component and TitleBar prop wiring; CSS adds .capability-badge variants and aligns .title-end.
Native settings section and provider controls
viewer/openworlds/screen-settings.jsx, viewer/openworlds/screen-launcher.jsx
Adds a "native" settings section with NativeAppSection (status, action buttons, provider controls, diagnostics) and simplifies launcher resume handling (no cross-origin redirects).
Server routing and test assertions
viewer/server.py, viewer/tests/test_openworlds_static.py
Adds internal redirect helper for exact /_OPENWORLDS_ROUTE requests; refactors test HTTP helpers to return headers and asserts native-bridge.js is present in the /openworlds/ bundle.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(macos): make OpenWorlds the native app shell' accurately describes the primary architectural change—converting OpenWorlds from a browser app to the main native macOS app shell while keeping Swift/AppKit as supervisor infrastructure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • LINEAR integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2e14672 and b0dddfc.

📒 Files selected for processing (15)
  • docs/OPENWORLDS_NATIVE_APP_ROADMAP.md
  • macos/ClawDnDApp/Sources/ClawDnDApp/App/ClawDnDApp.swift
  • macos/ClawDnDApp/Sources/ClawDnDApp/Models/LocalEndpoint.swift
  • macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift
  • macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift
  • viewer/openworlds/SOURCE.md
  • viewer/openworlds/app.jsx
  • viewer/openworlds/chrome.jsx
  • viewer/openworlds/index.html
  • viewer/openworlds/native-bridge.js
  • viewer/openworlds/screen-launcher.jsx
  • viewer/openworlds/screen-settings.jsx
  • viewer/openworlds/styles.css
  • viewer/server.py
  • viewer/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.swift handlers):
    • 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:

  1. refresh() — refresh dependencies and reload campaign store
  2. startOpenWorlds() — launch local viewer via processService
  3. waitForOpenWorlds() — poll local URL with fixed timeout until ready
  4. Display OpenWorldsLaunchOverlay during startup or on errors
  5. Load WebView once 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 win

Native bridge request names match implementation (no startOpenWorlds/stopOpenWorlds drift)

docs/OPENWORLDS_NATIVE_APP_ROADMAP.md’s startViewer/stopViewer list matches the actual bridge request.type handling in macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift and the JS callers in viewer/openworlds/screen-settings.jsx. The startOpenWorlds hits in RootView.swift are 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!

  • NativeBridgeRequest validation is sound (type required, requestId fallback, payload defaults)
  • NativeBridgeReply factory methods and dictionary export are clean
  • JS bridge uses IIFE to avoid globals pollution, handles missing crypto.randomUUID, and dispatches both callback resolution and CustomEvent

Also 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() in onDisappear prevents 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!

  • nativeState initialization correctly checks bridge availability synchronously
  • refreshNative properly handles both success and failure cases with appropriate state updates
  • Effect cleanup correctly removes event listener and clears interval
  • capabilityForScreen provides clear per-screen capability classification

Also applies to: 74-107, 252-265


167-168: LGTM!

Props correctly threaded to TitleBar and ScreenRouter, 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 win

Confirm native action names match the Swift bridge (no change needed).

  • nativeAction("startViewer") / nativeAction("stopViewer") in viewer/openworlds/screen-settings.jsx map to case "startViewer" / case "stopViewer" in macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift.
  • nativeAction("stopProvider") maps to case "stopProvider" in RootView.swift.
  • nativeAction("startProviderSession") (line 246) maps to case "startProviderSession" in RootView.swift.
  • The proposed renaming to startOpenWorlds / stopOpenWorlds / stopProviderSession is 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>

Comment thread macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift Outdated
Comment thread macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift
Comment thread macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between b0dddfc and 80a45d8.

📒 Files selected for processing (3)
  • docs/OPENWORLDS_NATIVE_APP_ROADMAP.md
  • macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift
  • macos/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())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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 | head

Repository: 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 50

Repository: 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 120

Repository: 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 260

Repository: 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 200

Repository: 100yenadmin/ClawDnD

Length of output: 1864


Restore native titlebar move/close/minimize/zoom affordances (or add a dedicated drag region) in RootView

  • OpenWorldsWindowChrome.configure removes window.styleMask.remove(.titled), hides the standard close/miniaturize/zoom buttons, and sets window.toolbar = nil, leaving only window.isMovableByWindowBackground = true for window movement.
  • RootView renders a full-screen WebView (WKWebView) via ZStack { ... 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.

Comment on lines +76 to +79
if nativeRequestHandler != nil {
configuration.userContentController.addUserScript(Self.nativeBridgeScript)
configuration.userContentController.add(context.coordinator, name: "clawdnd")
context.coordinator.hasNativeMessageHandler = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 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 || true

Repository: 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 || true

Repository: 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.jsx

Repository: 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.swift registers a WKScriptMessageHandler named clawdnd and Coordinator.userContentController(_:didReceive:) dispatches request.type directly to privileged actions (startViewer, stopViewer, startProviderSession, stopProvider, diagnostics/copyDiagnostics, and openFallbackDashboard).
  • There’s no navigation-policy/origin allowlist: the WKNavigationDelegate methods only report load failures, and userContentController(_:didReceive:) doesn’t check webView.url/origin before executing the request.
  • Although nativeBridgeScript is injected with forMainFrameOnly: 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 when webView?.url matches the expected local OpenWorlds scheme/host (and optionally path).
    • In Coordinator implement webView(_:decidePolicyFor:decisionHandler:) to cancel any top-level navigation that doesn’t match that allowlist (and consider removing/temporarily disabling the handler when navigating away).
🤖 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant