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
167 lines
6.4 KiB
Swift
167 lines
6.4 KiB
Swift
import AppKit
|
|
import OpenClawIPC
|
|
|
|
extension CanvasWindowController {
|
|
// MARK: - Window
|
|
|
|
static func makeWindow(for presentation: CanvasPresentation, contentView: NSView) -> NSWindow {
|
|
switch presentation {
|
|
case .window:
|
|
let window = NSWindow(
|
|
contentRect: NSRect(origin: .zero, size: CanvasLayout.windowSize),
|
|
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
|
backing: .buffered,
|
|
defer: false)
|
|
window.title = "OpenClaw Canvas"
|
|
window.isReleasedWhenClosed = false
|
|
window.contentView = contentView
|
|
window.center()
|
|
window.minSize = NSSize(width: 880, height: 680)
|
|
return window
|
|
|
|
case .panel:
|
|
let panel = CanvasPanel(
|
|
contentRect: NSRect(origin: .zero, size: CanvasLayout.panelSize),
|
|
styleMask: [.borderless, .resizable],
|
|
backing: .buffered,
|
|
defer: false)
|
|
// Keep Canvas below the Voice Wake overlay panel.
|
|
panel.level = NSWindow.Level(rawValue: NSWindow.Level.statusBar.rawValue - 1)
|
|
panel.hasShadow = true
|
|
panel.isMovable = false
|
|
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
|
panel.titleVisibility = .hidden
|
|
panel.titlebarAppearsTransparent = true
|
|
panel.backgroundColor = .clear
|
|
panel.isOpaque = false
|
|
panel.contentView = contentView
|
|
panel.becomesKeyOnlyIfNeeded = true
|
|
panel.hidesOnDeactivate = false
|
|
panel.minSize = CanvasLayout.minPanelSize
|
|
return panel
|
|
}
|
|
}
|
|
|
|
func presentAnchoredPanel(anchorProvider: @escaping () -> NSRect?) {
|
|
guard case .panel = self.presentation, let window else { return }
|
|
self.repositionPanel(using: anchorProvider)
|
|
window.makeKeyAndOrderFront(nil)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
window.makeFirstResponder(self.webView)
|
|
VoiceWakeOverlayController.shared.bringToFrontIfVisible()
|
|
self.onVisibilityChanged?(true)
|
|
}
|
|
|
|
func repositionPanel(using anchorProvider: () -> NSRect?) {
|
|
guard let panel = self.window else { return }
|
|
let anchor = anchorProvider()
|
|
let targetScreen = Self.screen(forAnchor: anchor)
|
|
?? Self.screenContainingMouseCursor()
|
|
?? panel.screen
|
|
?? NSScreen.main
|
|
?? NSScreen.screens.first
|
|
|
|
let restored = Self.loadRestoredFrame(sessionKey: self.sessionKey)
|
|
let restoredIsValid = if let restored, let targetScreen {
|
|
Self.isFrameMeaningfullyVisible(restored, on: targetScreen)
|
|
} else {
|
|
restored != nil
|
|
}
|
|
|
|
var frame = if let restored, restoredIsValid {
|
|
restored
|
|
} else {
|
|
Self.defaultTopRightFrame(panel: panel, screen: targetScreen)
|
|
}
|
|
|
|
// Apply agent placement as partial overrides:
|
|
// - If agent provides x/y, override origin.
|
|
// - If agent provides width/height, override size.
|
|
// - If agent provides only size, keep the remembered origin.
|
|
if let placement = self.preferredPlacement {
|
|
if let x = placement.x { frame.origin.x = x }
|
|
if let y = placement.y { frame.origin.y = y }
|
|
if let w = placement.width { frame.size.width = max(CanvasLayout.minPanelSize.width, CGFloat(w)) }
|
|
if let h = placement.height { frame.size.height = max(CanvasLayout.minPanelSize.height, CGFloat(h)) }
|
|
}
|
|
|
|
self.setPanelFrame(frame, on: targetScreen)
|
|
}
|
|
|
|
static func defaultTopRightFrame(panel: NSWindow, screen: NSScreen?) -> NSRect {
|
|
let w = max(CanvasLayout.minPanelSize.width, panel.frame.width)
|
|
let h = max(CanvasLayout.minPanelSize.height, panel.frame.height)
|
|
return WindowPlacement.topRightFrame(
|
|
size: NSSize(width: w, height: h),
|
|
padding: CanvasLayout.defaultPadding,
|
|
on: screen)
|
|
}
|
|
|
|
func setPanelFrame(_ frame: NSRect, on screen: NSScreen?) {
|
|
guard let panel = self.window else { return }
|
|
guard let s = screen ?? panel.screen ?? NSScreen.main ?? NSScreen.screens.first else {
|
|
panel.setFrame(frame, display: false)
|
|
self.persistFrameIfPanel()
|
|
return
|
|
}
|
|
|
|
let constrained = Self.constrainFrame(frame, toVisibleFrame: s.visibleFrame)
|
|
panel.setFrame(constrained, display: false)
|
|
self.persistFrameIfPanel()
|
|
}
|
|
|
|
static func screen(forAnchor anchor: NSRect?) -> NSScreen? {
|
|
guard let anchor else { return nil }
|
|
let center = NSPoint(x: anchor.midX, y: anchor.midY)
|
|
return NSScreen.screens.first { screen in
|
|
screen.frame.contains(anchor.origin) || screen.frame.contains(center)
|
|
}
|
|
}
|
|
|
|
static func screenContainingMouseCursor() -> NSScreen? {
|
|
let point = NSEvent.mouseLocation
|
|
return NSScreen.screens.first { $0.frame.contains(point) }
|
|
}
|
|
|
|
static func isFrameMeaningfullyVisible(_ frame: NSRect, on screen: NSScreen) -> Bool {
|
|
frame.intersects(screen.visibleFrame.insetBy(dx: 12, dy: 12))
|
|
}
|
|
|
|
static func constrainFrame(_ frame: NSRect, toVisibleFrame bounds: NSRect) -> NSRect {
|
|
if bounds == .zero { return frame }
|
|
|
|
var next = frame
|
|
next.size.width = min(max(CanvasLayout.minPanelSize.width, next.size.width), bounds.width)
|
|
next.size.height = min(max(CanvasLayout.minPanelSize.height, next.size.height), bounds.height)
|
|
|
|
let maxX = bounds.maxX - next.size.width
|
|
let maxY = bounds.maxY - next.size.height
|
|
|
|
next.origin.x = maxX >= bounds.minX ? min(max(next.origin.x, bounds.minX), maxX) : bounds.minX
|
|
next.origin.y = maxY >= bounds.minY ? min(max(next.origin.y, bounds.minY), maxY) : bounds.minY
|
|
|
|
next.origin.x = round(next.origin.x)
|
|
next.origin.y = round(next.origin.y)
|
|
return next
|
|
}
|
|
|
|
// MARK: - NSWindowDelegate
|
|
|
|
func windowWillClose(_: Notification) {
|
|
self.onVisibilityChanged?(false)
|
|
}
|
|
|
|
func windowDidMove(_: Notification) {
|
|
self.persistFrameIfPanel()
|
|
}
|
|
|
|
func windowDidEndLiveResize(_: Notification) {
|
|
self.persistFrameIfPanel()
|
|
}
|
|
|
|
func persistFrameIfPanel() {
|
|
guard case .panel = self.presentation, let window else { return }
|
|
Self.storeRestoredFrame(window.frame, sessionKey: self.sessionKey)
|
|
}
|
|
}
|