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:
490
apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift
Normal file
490
apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift
Normal file
@@ -0,0 +1,490 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import OpenClawChatUI
|
||||
import OpenClawKit
|
||||
import OpenClawProtocol
|
||||
import OSLog
|
||||
import QuartzCore
|
||||
import SwiftUI
|
||||
|
||||
private let webChatSwiftLogger = Logger(subsystem: "ai.openclaw", category: "WebChatSwiftUI")
|
||||
private let webChatThinkingLevelDefaultsKey = "openclaw.webchat.thinkingLevel"
|
||||
|
||||
private enum WebChatSwiftUILayout {
|
||||
static let windowSize = NSSize(width: 500, height: 840)
|
||||
static let panelSize = NSSize(width: 480, height: 640)
|
||||
static let windowMinSize = NSSize(width: 480, height: 360)
|
||||
static let anchorPadding: CGFloat = 8
|
||||
}
|
||||
|
||||
struct MacGatewayChatTransport: OpenClawChatTransport {
|
||||
func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload {
|
||||
try await GatewayConnection.shared.chatHistory(sessionKey: sessionKey)
|
||||
}
|
||||
|
||||
func listModels() async throws -> [OpenClawChatModelChoice] {
|
||||
do {
|
||||
let data = try await GatewayConnection.shared.request(
|
||||
method: "models.list",
|
||||
params: [:],
|
||||
timeoutMs: 15000)
|
||||
let result = try JSONDecoder().decode(ModelsListResult.self, from: data)
|
||||
return result.models.map(Self.mapModelChoice)
|
||||
} catch {
|
||||
webChatSwiftLogger.warning(
|
||||
"models.list failed; hiding model picker: \(error.localizedDescription, privacy: .public)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func abortRun(sessionKey: String, runId: String) async throws {
|
||||
_ = try await GatewayConnection.shared.request(
|
||||
method: "chat.abort",
|
||||
params: [
|
||||
"sessionKey": AnyCodable(sessionKey),
|
||||
"runId": AnyCodable(runId),
|
||||
],
|
||||
timeoutMs: 10000)
|
||||
}
|
||||
|
||||
func listSessions(limit: Int?) async throws -> OpenClawChatSessionsListResponse {
|
||||
var params: [String: AnyCodable] = [
|
||||
"includeGlobal": AnyCodable(true),
|
||||
"includeUnknown": AnyCodable(false),
|
||||
]
|
||||
if let limit {
|
||||
params["limit"] = AnyCodable(limit)
|
||||
}
|
||||
let data = try await GatewayConnection.shared.request(
|
||||
method: "sessions.list",
|
||||
params: params,
|
||||
timeoutMs: 15000)
|
||||
let decoded = try JSONDecoder().decode(OpenClawChatSessionsListResponse.self, from: data)
|
||||
let mainSessionKey = await GatewayConnection.shared.cachedMainSessionKey()
|
||||
let defaults = decoded.defaults.map {
|
||||
OpenClawChatSessionsDefaults(
|
||||
modelProvider: $0.modelProvider,
|
||||
model: $0.model,
|
||||
contextTokens: $0.contextTokens,
|
||||
thinkingLevels: $0.thinkingLevels,
|
||||
thinkingOptions: $0.thinkingOptions,
|
||||
thinkingDefault: $0.thinkingDefault,
|
||||
mainSessionKey: mainSessionKey)
|
||||
} ?? OpenClawChatSessionsDefaults(
|
||||
model: nil,
|
||||
contextTokens: nil,
|
||||
mainSessionKey: mainSessionKey)
|
||||
return OpenClawChatSessionsListResponse(
|
||||
ts: decoded.ts,
|
||||
path: decoded.path,
|
||||
count: decoded.count,
|
||||
defaults: defaults,
|
||||
sessions: decoded.sessions)
|
||||
}
|
||||
|
||||
func setSessionModel(sessionKey: String, model: String?) async throws {
|
||||
var params: [String: AnyCodable] = [
|
||||
"key": AnyCodable(sessionKey),
|
||||
]
|
||||
params["model"] = model.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
||||
_ = try await GatewayConnection.shared.request(
|
||||
method: "sessions.patch",
|
||||
params: params,
|
||||
timeoutMs: 15000)
|
||||
}
|
||||
|
||||
func setSessionThinking(sessionKey: String, thinkingLevel: String) async throws {
|
||||
let params: [String: AnyCodable] = [
|
||||
"key": AnyCodable(sessionKey),
|
||||
"thinkingLevel": AnyCodable(thinkingLevel),
|
||||
]
|
||||
_ = try await GatewayConnection.shared.request(
|
||||
method: "sessions.patch",
|
||||
params: params,
|
||||
timeoutMs: 15000)
|
||||
}
|
||||
|
||||
func sendMessage(
|
||||
sessionKey: String,
|
||||
message: String,
|
||||
thinking: String,
|
||||
idempotencyKey: String,
|
||||
attachments: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse
|
||||
{
|
||||
try await GatewayConnection.shared.chatSend(
|
||||
sessionKey: sessionKey,
|
||||
message: message,
|
||||
thinking: thinking,
|
||||
idempotencyKey: idempotencyKey,
|
||||
attachments: attachments)
|
||||
}
|
||||
|
||||
func requestHealth(timeoutMs: Int) async throws -> Bool {
|
||||
try await GatewayConnection.shared.healthOK(timeoutMs: timeoutMs)
|
||||
}
|
||||
|
||||
func resetSession(sessionKey: String) async throws {
|
||||
_ = try await GatewayConnection.shared.request(
|
||||
method: "sessions.reset",
|
||||
params: ["key": AnyCodable(sessionKey)],
|
||||
timeoutMs: 10000)
|
||||
}
|
||||
|
||||
func compactSession(sessionKey: String) async throws {
|
||||
let response = try await GatewayConnection.shared.request(
|
||||
method: "sessions.compact",
|
||||
params: ["key": AnyCodable(sessionKey)],
|
||||
timeoutMs: 0,
|
||||
retryTransportFailures: false)
|
||||
try OpenClawSessionsCompactResponse.requireSuccess(from: response)
|
||||
}
|
||||
|
||||
func setActiveSessionKey(_ sessionKey: String) async throws {
|
||||
await MainActor.run {
|
||||
WebChatManager.shared.recordActiveSessionKey(sessionKey)
|
||||
}
|
||||
_ = try await GatewayConnection.shared.request(
|
||||
method: "sessions.messages.subscribe",
|
||||
params: ["key": AnyCodable(sessionKey)],
|
||||
timeoutMs: 10000)
|
||||
}
|
||||
|
||||
func events() -> AsyncStream<OpenClawChatTransportEvent> {
|
||||
AsyncStream { continuation in
|
||||
let task = Task {
|
||||
do {
|
||||
try await GatewayConnection.shared.refresh()
|
||||
} catch {
|
||||
webChatSwiftLogger.error("gateway refresh failed \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
||||
let stream = await GatewayConnection.shared.subscribe()
|
||||
for await push in stream {
|
||||
if Task.isCancelled { return }
|
||||
if let evt = Self.mapPushToTransportEvent(push) {
|
||||
continuation.yield(evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
continuation.onTermination = { @Sendable _ in
|
||||
task.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func mapPushToTransportEvent(_ push: GatewayPush) -> OpenClawChatTransportEvent? {
|
||||
switch push {
|
||||
case let .snapshot(hello):
|
||||
let ok = (try? JSONDecoder().decode(
|
||||
OpenClawGatewayHealthOK.self,
|
||||
from: JSONEncoder().encode(hello.snapshot.health)))?.ok ?? true
|
||||
return .health(ok: ok)
|
||||
|
||||
case let .event(evt):
|
||||
switch evt.event {
|
||||
case "health":
|
||||
guard let payload = evt.payload else { return nil }
|
||||
let ok = (try? JSONDecoder().decode(
|
||||
OpenClawGatewayHealthOK.self,
|
||||
from: JSONEncoder().encode(payload)))?.ok ?? true
|
||||
return .health(ok: ok)
|
||||
case "tick":
|
||||
return .tick
|
||||
case "chat":
|
||||
guard let payload = evt.payload else { return nil }
|
||||
guard let chat = try? JSONDecoder().decode(
|
||||
OpenClawChatEventPayload.self,
|
||||
from: JSONEncoder().encode(payload))
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return .chat(chat)
|
||||
case "session.message":
|
||||
guard let payload = evt.payload else { return nil }
|
||||
guard let message = try? JSONDecoder().decode(
|
||||
OpenClawSessionMessageEventPayload.self,
|
||||
from: JSONEncoder().encode(payload))
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return .sessionMessage(message)
|
||||
case "agent":
|
||||
guard let payload = evt.payload else { return nil }
|
||||
guard let agent = try? JSONDecoder().decode(
|
||||
OpenClawAgentEventPayload.self,
|
||||
from: JSONEncoder().encode(payload))
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return .agent(agent)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
case .seqGap:
|
||||
return .seqGap
|
||||
}
|
||||
}
|
||||
|
||||
private static func mapModelChoice(_ model: OpenClawProtocol.ModelChoice) -> OpenClawChatModelChoice {
|
||||
OpenClawChatModelChoice(
|
||||
modelID: model.id,
|
||||
name: model.name,
|
||||
provider: model.provider,
|
||||
contextWindow: model.contextwindow)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Window controller
|
||||
|
||||
@MainActor
|
||||
final class WebChatSwiftUIWindowController {
|
||||
private let presentation: WebChatPresentation
|
||||
private let sessionKey: String
|
||||
private let hosting: NSHostingController<OpenClawChatView>
|
||||
private let contentController: NSViewController
|
||||
private var window: NSWindow?
|
||||
private var dismissMonitor: Any?
|
||||
var onClosed: (() -> Void)?
|
||||
var onVisibilityChanged: ((Bool) -> Void)?
|
||||
|
||||
convenience init(sessionKey: String, presentation: WebChatPresentation) {
|
||||
self.init(sessionKey: sessionKey, presentation: presentation, transport: MacGatewayChatTransport())
|
||||
}
|
||||
|
||||
init(sessionKey: String, presentation: WebChatPresentation, transport: any OpenClawChatTransport) {
|
||||
self.sessionKey = sessionKey
|
||||
self.presentation = presentation
|
||||
let vm = OpenClawChatViewModel(
|
||||
sessionKey: sessionKey,
|
||||
transport: transport,
|
||||
initialThinkingLevel: Self.persistedThinkingLevel(),
|
||||
onThinkingLevelChanged: { level in
|
||||
UserDefaults.standard.set(level, forKey: webChatThinkingLevelDefaultsKey)
|
||||
})
|
||||
let accent = Self.color(fromHex: AppStateStore.shared.seamColorHex)
|
||||
self.hosting = NSHostingController(rootView: OpenClawChatView(
|
||||
viewModel: vm,
|
||||
showsSessionSwitcher: true,
|
||||
userAccent: accent))
|
||||
self.contentController = Self.makeContentController(for: presentation, hosting: self.hosting)
|
||||
self.window = Self.makeWindow(for: presentation, contentViewController: self.contentController)
|
||||
}
|
||||
|
||||
deinit {}
|
||||
|
||||
var isVisible: Bool {
|
||||
self.window?.isVisible ?? false
|
||||
}
|
||||
|
||||
func show() {
|
||||
guard let window else { return }
|
||||
self.ensureWindowSize()
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
self.onVisibilityChanged?(true)
|
||||
}
|
||||
|
||||
func presentAnchored(anchorProvider: () -> NSRect?) {
|
||||
guard case .panel = self.presentation, let window else { return }
|
||||
self.installDismissMonitor()
|
||||
let target = self.reposition(using: anchorProvider)
|
||||
|
||||
if !self.isVisible {
|
||||
let start = target.offsetBy(dx: 0, dy: 8)
|
||||
window.setFrame(start, display: true)
|
||||
window.alphaValue = 0
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
NSAnimationContext.runAnimationGroup { context in
|
||||
context.duration = 0.18
|
||||
context.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||
window.animator().setFrame(target, display: true)
|
||||
window.animator().alphaValue = 1
|
||||
}
|
||||
} else {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
self.onVisibilityChanged?(true)
|
||||
}
|
||||
|
||||
func close() {
|
||||
self.window?.orderOut(nil)
|
||||
self.onVisibilityChanged?(false)
|
||||
self.onClosed?()
|
||||
self.removeDismissMonitor()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func reposition(using anchorProvider: () -> NSRect?) -> NSRect {
|
||||
guard let window else { return .zero }
|
||||
guard let anchor = anchorProvider() else {
|
||||
let frame = WindowPlacement.topRightFrame(
|
||||
size: WebChatSwiftUILayout.panelSize,
|
||||
padding: WebChatSwiftUILayout.anchorPadding)
|
||||
window.setFrame(frame, display: false)
|
||||
return frame
|
||||
}
|
||||
let screen = NSScreen.screens.first { screen in
|
||||
screen.frame.contains(anchor.origin) || screen.frame.contains(NSPoint(x: anchor.midX, y: anchor.midY))
|
||||
} ?? NSScreen.main
|
||||
let bounds = (screen?.visibleFrame ?? .zero).insetBy(
|
||||
dx: WebChatSwiftUILayout.anchorPadding,
|
||||
dy: WebChatSwiftUILayout.anchorPadding)
|
||||
let frame = WindowPlacement.anchoredBelowFrame(
|
||||
size: WebChatSwiftUILayout.panelSize,
|
||||
anchor: anchor,
|
||||
padding: WebChatSwiftUILayout.anchorPadding,
|
||||
in: bounds)
|
||||
window.setFrame(frame, display: false)
|
||||
return frame
|
||||
}
|
||||
|
||||
private func installDismissMonitor() {
|
||||
if ProcessInfo.processInfo.isRunningTests { return }
|
||||
guard self.dismissMonitor == nil, self.window != nil else { return }
|
||||
self.dismissMonitor = NSEvent.addGlobalMonitorForEvents(
|
||||
matching: [.leftMouseDown, .rightMouseDown, .otherMouseDown])
|
||||
{ [weak self] _ in
|
||||
guard let self, let win = self.window else { return }
|
||||
let pt = NSEvent.mouseLocation
|
||||
if !win.frame.contains(pt) {
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeDismissMonitor() {
|
||||
OverlayPanelFactory.clearGlobalEventMonitor(&self.dismissMonitor)
|
||||
}
|
||||
|
||||
private static func persistedThinkingLevel() -> String? {
|
||||
let stored = UserDefaults.standard.string(forKey: webChatThinkingLevelDefaultsKey)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased()
|
||||
guard let stored, ["off", "minimal", "low", "medium", "high", "xhigh", "adaptive"].contains(stored) else {
|
||||
return nil
|
||||
}
|
||||
return stored
|
||||
}
|
||||
|
||||
private static func makeWindow(
|
||||
for presentation: WebChatPresentation,
|
||||
contentViewController: NSViewController) -> NSWindow
|
||||
{
|
||||
switch presentation {
|
||||
case .window:
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(origin: .zero, size: WebChatSwiftUILayout.windowSize),
|
||||
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
||||
backing: .buffered,
|
||||
defer: false)
|
||||
window.title = "OpenClaw Chat"
|
||||
window.contentViewController = contentViewController
|
||||
window.isReleasedWhenClosed = false
|
||||
window.titleVisibility = .visible
|
||||
window.titlebarAppearsTransparent = false
|
||||
window.backgroundColor = .clear
|
||||
window.isOpaque = false
|
||||
window.center()
|
||||
WindowPlacement.ensureOnScreen(window: window, defaultSize: WebChatSwiftUILayout.windowSize)
|
||||
window.minSize = WebChatSwiftUILayout.windowMinSize
|
||||
window.contentView?.wantsLayer = true
|
||||
window.contentView?.layer?.backgroundColor = NSColor.clear.cgColor
|
||||
return window
|
||||
case .panel:
|
||||
let panel = WebChatPanel(
|
||||
contentRect: NSRect(origin: .zero, size: WebChatSwiftUILayout.panelSize),
|
||||
styleMask: [.borderless],
|
||||
backing: .buffered,
|
||||
defer: false)
|
||||
panel.level = .statusBar
|
||||
panel.hidesOnDeactivate = true
|
||||
panel.hasShadow = true
|
||||
panel.isMovable = false
|
||||
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||
panel.titleVisibility = .hidden
|
||||
panel.titlebarAppearsTransparent = true
|
||||
panel.backgroundColor = .clear
|
||||
panel.isOpaque = false
|
||||
panel.contentViewController = contentViewController
|
||||
panel.becomesKeyOnlyIfNeeded = true
|
||||
panel.contentView?.wantsLayer = true
|
||||
panel.contentView?.layer?.backgroundColor = NSColor.clear.cgColor
|
||||
panel.setFrame(
|
||||
WindowPlacement.topRightFrame(
|
||||
size: WebChatSwiftUILayout.panelSize,
|
||||
padding: WebChatSwiftUILayout.anchorPadding),
|
||||
display: false)
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
private static func makeContentController(
|
||||
for presentation: WebChatPresentation,
|
||||
hosting: NSHostingController<OpenClawChatView>) -> NSViewController
|
||||
{
|
||||
let controller = NSViewController()
|
||||
let effectView = NSVisualEffectView()
|
||||
effectView.material = .sidebar
|
||||
effectView.blendingMode = switch presentation {
|
||||
case .panel:
|
||||
.withinWindow
|
||||
case .window:
|
||||
.behindWindow
|
||||
}
|
||||
effectView.state = .active
|
||||
effectView.wantsLayer = true
|
||||
effectView.layer?.cornerCurve = .continuous
|
||||
let cornerRadius: CGFloat = switch presentation {
|
||||
case .panel:
|
||||
16
|
||||
case .window:
|
||||
0
|
||||
}
|
||||
effectView.layer?.cornerRadius = cornerRadius
|
||||
effectView.layer?.masksToBounds = true
|
||||
effectView.layer?.backgroundColor = NSColor.clear.cgColor
|
||||
|
||||
effectView.translatesAutoresizingMaskIntoConstraints = true
|
||||
effectView.autoresizingMask = [.width, .height]
|
||||
let rootView = effectView
|
||||
|
||||
hosting.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
hosting.view.wantsLayer = true
|
||||
hosting.view.layer?.cornerCurve = .continuous
|
||||
hosting.view.layer?.cornerRadius = cornerRadius
|
||||
hosting.view.layer?.masksToBounds = true
|
||||
hosting.view.layer?.backgroundColor = NSColor.clear.cgColor
|
||||
|
||||
controller.addChild(hosting)
|
||||
effectView.addSubview(hosting.view)
|
||||
controller.view = rootView
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
hosting.view.leadingAnchor.constraint(equalTo: effectView.leadingAnchor),
|
||||
hosting.view.trailingAnchor.constraint(equalTo: effectView.trailingAnchor),
|
||||
hosting.view.topAnchor.constraint(equalTo: effectView.topAnchor),
|
||||
hosting.view.bottomAnchor.constraint(equalTo: effectView.bottomAnchor),
|
||||
])
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
private func ensureWindowSize() {
|
||||
guard case .window = self.presentation, let window else { return }
|
||||
let current = window.frame.size
|
||||
let min = WebChatSwiftUILayout.windowMinSize
|
||||
if current.width < min.width || current.height < min.height {
|
||||
let frame = WindowPlacement.centeredFrame(size: WebChatSwiftUILayout.windowSize)
|
||||
window.setFrame(frame, display: false)
|
||||
}
|
||||
}
|
||||
|
||||
private static func color(fromHex raw: String?) -> Color? {
|
||||
ColorHexSupport.color(fromHex: raw)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user