Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
439
apps/macos/Sources/OpenClaw/DashboardWindowController.swift
Normal file
439
apps/macos/Sources/OpenClaw/DashboardWindowController.swift
Normal file
@@ -0,0 +1,439 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import WebKit
|
||||
|
||||
private final class DashboardWindowContentView: NSView {
|
||||
override var mouseDownCanMoveWindow: Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private final class DashboardWindowDragRegionView: NSView {
|
||||
override var mouseDownCanMoveWindow: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
self.window?.performDrag(with: event)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class DashboardWindowController: NSWindowController, WKNavigationDelegate, WKUIDelegate, NSWindowDelegate {
|
||||
private let webView: WKWebView
|
||||
private var currentURL: URL
|
||||
private var auth: DashboardWindowAuth
|
||||
|
||||
init(url: URL, auth: DashboardWindowAuth) {
|
||||
self.currentURL = url
|
||||
self.auth = auth
|
||||
|
||||
let config = WKWebViewConfiguration()
|
||||
config.preferences.isElementFullscreenEnabled = true
|
||||
config.preferences.setValue(true, forKey: "developerExtrasEnabled")
|
||||
config.userContentController = WKUserContentController()
|
||||
Self.installNativeChromeScript(into: config.userContentController)
|
||||
Self.installNativeAuthScript(into: config.userContentController, url: url, auth: auth)
|
||||
|
||||
self.webView = WKWebView(
|
||||
frame: NSRect(origin: .zero, size: DashboardWindowLayout.windowSize),
|
||||
configuration: config)
|
||||
self.webView.setValue(true, forKey: "drawsBackground")
|
||||
|
||||
let window = Self.makeWindow(contentView: self.webView)
|
||||
super.init(window: window)
|
||||
|
||||
self.webView.navigationDelegate = self
|
||||
self.webView.uiDelegate = self
|
||||
self.window?.delegate = self
|
||||
}
|
||||
|
||||
// MARK: - WKUIDelegate
|
||||
|
||||
/// Bridges `<input type="file">` clicks in the embedded Control UI to a native
|
||||
/// `NSOpenPanel`; without a `WKUIDelegate`, WebKit silently drops the request
|
||||
/// and "Choose image" / file-picker buttons do nothing.
|
||||
func webView(
|
||||
_ webView: WKWebView,
|
||||
runOpenPanelWith parameters: WKOpenPanelParameters,
|
||||
initiatedByFrame frame: WKFrameInfo,
|
||||
completionHandler: @escaping @MainActor @Sendable ([URL]?) -> Void)
|
||||
{
|
||||
let panel = NSOpenPanel()
|
||||
panel.canChooseFiles = true
|
||||
panel.canChooseDirectories = parameters.allowsDirectories
|
||||
panel.allowsMultipleSelection = parameters.allowsMultipleSelection
|
||||
panel.resolvesAliases = true
|
||||
if let window = self.window {
|
||||
panel.beginSheetModal(for: window) { response in
|
||||
completionHandler(response == .OK ? panel.urls : nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
panel.begin { response in
|
||||
completionHandler(response == .OK ? panel.urls : nil)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
}
|
||||
|
||||
func show(url: URL, auth: DashboardWindowAuth) {
|
||||
self.currentURL = url
|
||||
self.auth = auth
|
||||
self.refreshNativeAuthScript(url: url, auth: auth)
|
||||
self.load(url)
|
||||
self.show()
|
||||
}
|
||||
|
||||
func show() {
|
||||
if let window {
|
||||
let frame = window.frame
|
||||
if frame.width < DashboardWindowLayout.windowMinSize.width ||
|
||||
frame.height < DashboardWindowLayout.windowMinSize.height
|
||||
{
|
||||
window.setFrame(WindowPlacement.centeredFrame(size: DashboardWindowLayout.windowSize), display: false)
|
||||
}
|
||||
}
|
||||
self.showWindow(nil)
|
||||
self.window?.makeKeyAndOrderFront(nil)
|
||||
self.window?.makeFirstResponder(self.webView)
|
||||
self.window?.orderFrontRegardless()
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
func closeDashboard() {
|
||||
self.window?.performClose(nil)
|
||||
}
|
||||
|
||||
func showFailure(title: String, message: String, detail: String? = nil) {
|
||||
self.currentURL = URL(string: "about:blank")!
|
||||
self.auth = DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil)
|
||||
self.refreshNativeAuthScript(url: self.currentURL, auth: self.auth)
|
||||
self.webView.stopLoading()
|
||||
self.webView.loadHTMLString(
|
||||
Self.failureHTML(title: title, message: message, detail: detail, url: nil),
|
||||
baseURL: nil)
|
||||
self.show()
|
||||
}
|
||||
|
||||
private func load(_ url: URL) {
|
||||
dashboardWindowLogger.debug("dashboard load \(url.absoluteString, privacy: .public)")
|
||||
self.webView.load(URLRequest(url: url))
|
||||
}
|
||||
|
||||
private func refreshNativeAuthScript(url: URL, auth: DashboardWindowAuth) {
|
||||
let controller = self.webView.configuration.userContentController
|
||||
controller.removeAllUserScripts()
|
||||
Self.installNativeChromeScript(into: controller)
|
||||
Self.installNativeAuthScript(into: controller, url: url, auth: auth)
|
||||
}
|
||||
|
||||
private static func makeWindow(contentView: NSView) -> NSWindow {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(origin: .zero, size: DashboardWindowLayout.windowSize),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||
backing: .buffered,
|
||||
defer: false)
|
||||
let container = DashboardWindowContentView(frame: NSRect(origin: .zero, size: DashboardWindowLayout.windowSize))
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addSubview(contentView)
|
||||
let topDragRegion = DashboardWindowDragRegionView()
|
||||
topDragRegion.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addSubview(topDragRegion)
|
||||
let topRightDragRegion = DashboardWindowDragRegionView()
|
||||
topRightDragRegion.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addSubview(topRightDragRegion)
|
||||
let sidebarDragRegion = DashboardWindowDragRegionView()
|
||||
sidebarDragRegion.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addSubview(sidebarDragRegion)
|
||||
NSLayoutConstraint.activate([
|
||||
contentView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||
contentView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||
contentView.topAnchor.constraint(equalTo: container.topAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||
topDragRegion.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 78),
|
||||
topDragRegion.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -380),
|
||||
topDragRegion.topAnchor.constraint(equalTo: container.topAnchor),
|
||||
topDragRegion.heightAnchor.constraint(equalToConstant: 28),
|
||||
topRightDragRegion.leadingAnchor.constraint(equalTo: topDragRegion.trailingAnchor),
|
||||
topRightDragRegion.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -8),
|
||||
topRightDragRegion.topAnchor.constraint(equalTo: container.topAnchor),
|
||||
topRightDragRegion.heightAnchor.constraint(equalToConstant: 6),
|
||||
sidebarDragRegion.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 78),
|
||||
sidebarDragRegion.topAnchor.constraint(equalTo: container.topAnchor),
|
||||
sidebarDragRegion.widthAnchor.constraint(equalToConstant: 176),
|
||||
sidebarDragRegion.heightAnchor.constraint(equalToConstant: 46),
|
||||
])
|
||||
window.title = "OpenClaw"
|
||||
window.titleVisibility = .hidden
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.isMovableByWindowBackground = true
|
||||
window.isReleasedWhenClosed = false
|
||||
window.hasShadow = true
|
||||
window.backgroundColor = .windowBackgroundColor
|
||||
window.isOpaque = true
|
||||
let viewController = NSViewController()
|
||||
viewController.view = container
|
||||
window.contentViewController = viewController
|
||||
window.center()
|
||||
window.minSize = DashboardWindowLayout.windowMinSize
|
||||
WindowPlacement.ensureOnScreen(window: window, defaultSize: DashboardWindowLayout.windowSize)
|
||||
return window
|
||||
}
|
||||
|
||||
private static func installNativeChromeScript(into userContentController: WKUserContentController) {
|
||||
let css = """
|
||||
html.openclaw-native-macos {
|
||||
--openclaw-native-titlebar-height: 50px;
|
||||
}
|
||||
@media (min-width: 700px) {
|
||||
html.openclaw-native-macos .sidebar-shell {
|
||||
padding-top: max(14px, var(--openclaw-native-titlebar-height)) !important;
|
||||
}
|
||||
html.openclaw-native-macos .sidebar-shell__header {
|
||||
padding-left: 10px !important;
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
}
|
||||
"""
|
||||
let script = """
|
||||
(() => {
|
||||
try {
|
||||
if (document.getElementById("openclaw-native-macos-chrome")) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "openclaw-native-macos-chrome";
|
||||
style.textContent = \(Self.jsStringLiteral(css));
|
||||
document.documentElement.classList.add("openclaw-native-macos");
|
||||
document.head.appendChild(style);
|
||||
} catch {}
|
||||
})();
|
||||
"""
|
||||
userContentController.addUserScript(
|
||||
WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true))
|
||||
}
|
||||
|
||||
private static func installNativeAuthScript(
|
||||
into userContentController: WKUserContentController,
|
||||
url: URL,
|
||||
auth: DashboardWindowAuth)
|
||||
{
|
||||
guard auth.hasCredential else { return }
|
||||
let allowedOrigin = self.originString(for: url)
|
||||
let allowedPath = self.allowedPath(for: url)
|
||||
let payload: [String: Any?] = [
|
||||
"gatewayUrl": auth.gatewayUrl,
|
||||
"token": auth.token,
|
||||
"password": auth.password,
|
||||
]
|
||||
guard let data = try? JSONSerialization.data(withJSONObject: payload.compactMapValues { $0 }),
|
||||
let json = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
return
|
||||
}
|
||||
let script = """
|
||||
(() => {
|
||||
try {
|
||||
const allowedOrigin = \(Self.jsStringLiteral(allowedOrigin));
|
||||
const allowedPath = \(Self.jsStringLiteral(allowedPath));
|
||||
if (location.origin !== allowedOrigin) return;
|
||||
if (allowedPath !== "/" && !location.pathname.startsWith(allowedPath)) return;
|
||||
Object.defineProperty(window, "__OPENCLAW_NATIVE_CONTROL_AUTH__", {
|
||||
value: \(json),
|
||||
configurable: true,
|
||||
});
|
||||
} catch {}
|
||||
})();
|
||||
"""
|
||||
userContentController.addUserScript(
|
||||
WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: true))
|
||||
}
|
||||
|
||||
static func originString(for url: URL) -> String {
|
||||
guard let scheme = url.scheme, let host = url.host else { return "" }
|
||||
let hostPart = host.contains(":") && !host.hasPrefix("[") ? "[\(host)]" : host
|
||||
var out = "\(scheme)://\(hostPart)"
|
||||
if let port = url.port {
|
||||
out += ":\(port)"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private static func allowedPath(for url: URL) -> String {
|
||||
let path = url.path.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !path.isEmpty else { return "/" }
|
||||
return path.hasSuffix("/") ? path : path + "/"
|
||||
}
|
||||
|
||||
private static func jsStringLiteral(_ value: String) -> String {
|
||||
guard let data = try? JSONSerialization.data(withJSONObject: [value]),
|
||||
let raw = String(data: data, encoding: .utf8),
|
||||
raw.hasPrefix("["),
|
||||
raw.hasSuffix("]")
|
||||
else {
|
||||
return "\"\""
|
||||
}
|
||||
return String(raw.dropFirst().dropLast())
|
||||
}
|
||||
|
||||
func webView(
|
||||
_: WKWebView,
|
||||
decidePolicyFor navigationAction: WKNavigationAction,
|
||||
decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void)
|
||||
{
|
||||
guard let url = navigationAction.request.url else {
|
||||
decisionHandler(.allow)
|
||||
return
|
||||
}
|
||||
if Self.shouldAllowNavigation(to: url, dashboardURL: self.currentURL) {
|
||||
decisionHandler(.allow)
|
||||
return
|
||||
}
|
||||
NSWorkspace.shared.open(url)
|
||||
decisionHandler(.cancel)
|
||||
}
|
||||
|
||||
func webView(_: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
self.showLoadFailure(error)
|
||||
}
|
||||
|
||||
func webView(_: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
self.showLoadFailure(error)
|
||||
}
|
||||
|
||||
static func shouldAllowNavigation(to url: URL, dashboardURL: URL) -> Bool {
|
||||
guard let scheme = url.scheme?.lowercased() else { return true }
|
||||
if scheme == "about" || scheme == "blob" || scheme == "data" { return true }
|
||||
guard scheme == "http" || scheme == "https" else { return false }
|
||||
return url.scheme?.lowercased() == dashboardURL.scheme?.lowercased() &&
|
||||
url.host?.lowercased() == dashboardURL.host?.lowercased() &&
|
||||
url.port == dashboardURL.port
|
||||
}
|
||||
|
||||
func windowWillClose(_: Notification) {
|
||||
self.webView.stopLoading()
|
||||
}
|
||||
|
||||
private func showLoadFailure(_ error: Error) {
|
||||
let nsError = error as NSError
|
||||
if nsError.domain == NSURLErrorDomain, nsError.code == NSURLErrorCancelled { return }
|
||||
dashboardWindowLogger.error(
|
||||
"dashboard load failed url=\(self.currentURL.absoluteString, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
|
||||
let html = Self.failureHTML(
|
||||
title: "Dashboard unavailable",
|
||||
message: error.localizedDescription,
|
||||
detail: "The dashboard window is open, but the web UI could not load from this endpoint.",
|
||||
url: self.currentURL)
|
||||
self.webView.loadHTMLString(html, baseURL: nil)
|
||||
}
|
||||
|
||||
private static func failureHTML(title: String, message: String, detail: String?, url: URL?) -> String {
|
||||
let detailHTML = detail.map { "<p class=\"detail\">\(self.htmlEscape($0))</p>" } ?? ""
|
||||
let urlHTML = url.map { "<code>\(self.htmlEscape($0.absoluteString))</code>" } ?? ""
|
||||
return """
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #101114;
|
||||
color: rgba(255,255,255,.92);
|
||||
font: 15px -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
|
||||
}
|
||||
main {
|
||||
width: min(540px, calc(100vw - 72px));
|
||||
padding: 34px;
|
||||
border: 1px solid rgba(255,255,255,.12);
|
||||
border-radius: 22px;
|
||||
background: rgba(255,255,255,.035);
|
||||
box-shadow: 0 28px 90px rgba(0,0,0,.36);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.badge {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255,255,255,.07);
|
||||
color: #ff746b;
|
||||
font-size: 24px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 24px;
|
||||
line-height: 1.16;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: rgba(255,255,255,.76);
|
||||
font-size: 16px;
|
||||
}
|
||||
.detail {
|
||||
margin-top: 14px;
|
||||
color: rgba(255,255,255,.56);
|
||||
font-size: 13px;
|
||||
}
|
||||
code {
|
||||
display: block;
|
||||
margin-top: 18px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(255,255,255,.08);
|
||||
border-radius: 10px;
|
||||
background: rgba(0,0,0,.26);
|
||||
color: rgba(255,255,255,.76);
|
||||
overflow-wrap: anywhere;
|
||||
font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
body { background: #f5f6f8; color: rgba(0,0,0,.86); }
|
||||
main {
|
||||
background: rgba(255,255,255,.84);
|
||||
border-color: rgba(0,0,0,.1);
|
||||
box-shadow: 0 28px 90px rgba(0,0,0,.12);
|
||||
}
|
||||
.badge { background: rgba(0,0,0,.06); }
|
||||
p { color: rgba(0,0,0,.68); }
|
||||
.detail { color: rgba(0,0,0,.54); }
|
||||
code {
|
||||
background: rgba(0,0,0,.05);
|
||||
border-color: rgba(0,0,0,.08);
|
||||
color: rgba(0,0,0,.68);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="badge">!</div>
|
||||
<h1>\(self.htmlEscape(title))</h1>
|
||||
<p>\(self.htmlEscape(message))</p>
|
||||
\(detailHTML)
|
||||
\(urlHTML)
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
|
||||
private static func htmlEscape(_ value: String) -> String {
|
||||
value
|
||||
.replacingOccurrences(of: "&", with: "&")
|
||||
.replacingOccurrences(of: "<", with: "<")
|
||||
.replacingOccurrences(of: ">", with: ">")
|
||||
.replacingOccurrences(of: "\"", with: """)
|
||||
.replacingOccurrences(of: "'", with: "'")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user