From bd63b5a23114f5c2ce91a30e5566d007072265ae Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 19 Dec 2025 19:19:00 +0100 Subject: [PATCH] fix: show Dock icon during onboarding --- apps/macos/Sources/Clawdis/AppState.swift | 3 +- .../Sources/Clawdis/DockIconManager.swift | 117 ++++++++++++++++++ apps/macos/Sources/Clawdis/Onboarding.swift | 16 +-- 3 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 apps/macos/Sources/Clawdis/DockIconManager.swift diff --git a/apps/macos/Sources/Clawdis/AppState.swift b/apps/macos/Sources/Clawdis/AppState.swift index 37f81657c..7d1e111a4 100644 --- a/apps/macos/Sources/Clawdis/AppState.swift +++ b/apps/macos/Sources/Clawdis/AppState.swift @@ -390,6 +390,7 @@ enum AppStateStore { @MainActor enum AppActivationPolicy { static func apply(showDockIcon: Bool) { - NSApp.setActivationPolicy(showDockIcon ? .regular : .accessory) + _ = showDockIcon + DockIconManager.shared.updateDockVisibility() } } diff --git a/apps/macos/Sources/Clawdis/DockIconManager.swift b/apps/macos/Sources/Clawdis/DockIconManager.swift new file mode 100644 index 000000000..32122969a --- /dev/null +++ b/apps/macos/Sources/Clawdis/DockIconManager.swift @@ -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() + } + } +} diff --git a/apps/macos/Sources/Clawdis/Onboarding.swift b/apps/macos/Sources/Clawdis/Onboarding.swift index 238a636b6..bf7ca6780 100644 --- a/apps/macos/Sources/Clawdis/Onboarding.swift +++ b/apps/macos/Sources/Clawdis/Onboarding.swift @@ -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. " + - "We’ll 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@` 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() }