fix: show Dock icon during onboarding

This commit is contained in:
Peter Steinberger
2025-12-19 19:19:00 +01:00
parent 590f3d0e8f
commit bd63b5a231
3 changed files with 128 additions and 8 deletions

View File

@@ -390,6 +390,7 @@ enum AppStateStore {
@MainActor
enum AppActivationPolicy {
static func apply(showDockIcon: Bool) {
NSApp.setActivationPolicy(showDockIcon ? .regular : .accessory)
_ = showDockIcon
DockIconManager.shared.updateDockVisibility()
}
}

View File

@@ -0,0 +1,117 @@
import AppKit
import OSLog
/// Central manager for Dock icon visibility.
/// Shows the Dock icon while any windows are visible, regardless of user preference.
final class DockIconManager: NSObject, @unchecked Sendable {
static let shared = DockIconManager()
private var windowsObservation: NSKeyValueObservation?
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "DockIconManager")
private override init() {
super.init()
self.setupObservers()
Task { @MainActor in
self.updateDockVisibility()
}
}
deinit {
self.windowsObservation?.invalidate()
NotificationCenter.default.removeObserver(self)
}
func updateDockVisibility() {
Task { @MainActor in
guard NSApp != nil else {
self.logger.warning("NSApp not ready, skipping Dock visibility update")
return
}
let userWantsDockHidden = !UserDefaults.standard.bool(forKey: showDockIconKey)
let visibleWindows = NSApp?.windows.filter { window in
window.isVisible &&
window.frame.width > 1 &&
window.frame.height > 1 &&
!window.isKind(of: NSPanel.self) &&
"\(type(of: window))" != "NSPopupMenuWindow" &&
window.contentViewController != nil
} ?? []
let hasVisibleWindows = !visibleWindows.isEmpty
if !userWantsDockHidden || hasVisibleWindows {
NSApp?.setActivationPolicy(.regular)
} else {
NSApp?.setActivationPolicy(.accessory)
}
}
}
func temporarilyShowDock() {
Task { @MainActor in
guard NSApp != nil else {
self.logger.warning("NSApp not ready, cannot show Dock icon")
return
}
NSApp.setActivationPolicy(.regular)
}
}
private func setupObservers() {
Task { @MainActor in
guard let app = NSApp else {
self.logger.warning("NSApp not ready, delaying Dock observers")
try? await Task.sleep(for: .milliseconds(200))
self.setupObservers()
return
}
self.windowsObservation = app.observe(\.windows, options: [.new]) { [weak self] _, _ in
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(50))
self?.updateDockVisibility()
}
}
NotificationCenter.default.addObserver(
self,
selector: #selector(self.windowVisibilityChanged),
name: NSWindow.didBecomeKeyNotification,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.windowVisibilityChanged),
name: NSWindow.didResignKeyNotification,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.windowVisibilityChanged),
name: NSWindow.willCloseNotification,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.dockPreferenceChanged),
name: UserDefaults.didChangeNotification,
object: nil)
}
}
@objc
private func windowVisibilityChanged(_: Notification) {
Task { @MainActor in
self.updateDockVisibility()
}
}
@objc
private func dockPreferenceChanged(_ notification: Notification) {
guard let userDefaults = notification.object as? UserDefaults,
userDefaults == UserDefaults.standard
else { return }
Task { @MainActor in
self.updateDockVisibility()
}
}
}

View File

@@ -15,6 +15,7 @@ final class OnboardingController {
func show() {
if let window {
DockIconManager.shared.temporarilyShowDock()
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
return
@@ -28,6 +29,7 @@ final class OnboardingController {
window.titleVisibility = .hidden
window.isMovableByWindowBackground = true
window.center()
DockIconManager.shared.temporarilyShowDock()
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
self.window = window
@@ -630,7 +632,7 @@ struct OnboardingView: View {
.font(.largeTitle.weight(.semibold))
Text(
"The Gateway is the WebSocket service that keeps Clawdis connected. " +
"Well install/update the `clawdis` npm package and verify Node is available.")
"Clawdis bundles it and runs it via launchd so it stays running.")
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
@@ -674,7 +676,7 @@ struct OnboardingView: View {
if self.gatewayInstalling {
ProgressView()
} else {
Text("Install or update gateway")
Text("Enable Gateway daemon")
}
}
.buttonStyle(.borderedProminent)
@@ -692,7 +694,7 @@ struct OnboardingView: View {
.lineLimit(2)
} else {
Text(
"Runs `npm install -g clawdis@<version>` on your PATH. " +
"Installs a per-user LaunchAgent (\(gatewayLaunchdLabel)). " +
"The gateway listens on port 18789.")
.font(.caption)
.foregroundStyle(.secondary)
@@ -1248,10 +1250,10 @@ struct OnboardingView: View {
self.gatewayInstalling = true
defer { self.gatewayInstalling = false }
self.gatewayInstallMessage = nil
let expected = GatewayEnvironment.expectedGatewayVersion()
await GatewayEnvironment.installGlobal(version: expected) { message in
Task { @MainActor in self.gatewayInstallMessage = message }
}
let port = GatewayEnvironment.gatewayPort()
let bundlePath = Bundle.main.bundleURL.path
let err = await GatewayLaunchAgentManager.set(enabled: true, bundlePath: bundlePath, port: port)
self.gatewayInstallMessage = err ?? "Gateway enabled and started on port \(port)"
self.refreshGatewayStatus()
}