From bd69510662c2e02897ff4ee562dd90610d7259b3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 18 May 2026 09:15:10 +0100 Subject: [PATCH] feat(macos): add Dock menu shortcuts --- .../OpenClaw/AppNavigationActions.swift | 49 ++++++++++++++ apps/macos/Sources/OpenClaw/MenuBar.swift | 67 +++++++++++++++---- .../Sources/OpenClaw/MenuContentView.swift | 26 ++----- .../MenuContentSmokeTests.swift | 12 ++++ 4 files changed, 121 insertions(+), 33 deletions(-) create mode 100644 apps/macos/Sources/OpenClaw/AppNavigationActions.swift diff --git a/apps/macos/Sources/OpenClaw/AppNavigationActions.swift b/apps/macos/Sources/OpenClaw/AppNavigationActions.swift new file mode 100644 index 00000000000..e41a5660322 --- /dev/null +++ b/apps/macos/Sources/OpenClaw/AppNavigationActions.swift @@ -0,0 +1,49 @@ +import AppKit + +@MainActor +enum AppNavigationActions { + static func openDashboard() { + NSApp.activate(ignoringOtherApps: true) + if DashboardManager.shared.showConfiguredWindowIfPossible() { + return + } + Task { @MainActor in + if DashboardManager.shared.showConfiguredWindowIfPossible() { + return + } + do { + try await DashboardManager.shared.show() + } catch { + DashboardManager.shared.showFailure(error) + } + } + } + + static func openChat() { + NSApp.activate(ignoringOtherApps: true) + Task { @MainActor in + let sessionKey = await WebChatManager.shared.preferredSessionKey() + WebChatManager.shared.show(sessionKey: sessionKey) + } + } + + static func toggleCanvas() { + NSApp.activate(ignoringOtherApps: true) + Task { @MainActor in + if AppStateStore.shared.canvasPanelVisible { + CanvasManager.shared.hideAll() + } else { + let sessionKey = await GatewayConnection.shared.mainSessionKey() + _ = try? CanvasManager.shared.show(sessionKey: sessionKey, path: nil) + } + } + } + + static func openSettings(tab: SettingsTab = .general) { + SettingsTabRouter.request(tab) + SettingsWindowOpener.shared.open() + DispatchQueue.main.async { + NotificationCenter.default.post(name: .openclawSelectSettingsTab, object: tab) + } + } +} diff --git a/apps/macos/Sources/OpenClaw/MenuBar.swift b/apps/macos/Sources/OpenClaw/MenuBar.swift index 7ecfc362a04..7950451597a 100644 --- a/apps/macos/Sources/OpenClaw/MenuBar.swift +++ b/apps/macos/Sources/OpenClaw/MenuBar.swift @@ -173,19 +173,7 @@ struct OpenClawApp: App { private func openDashboardWindow() { HoverHUDController.shared.setSuppressed(true) self.isMenuPresented = false - if DashboardManager.shared.showConfiguredWindowIfPossible() { - return - } - Task { @MainActor in - if DashboardManager.shared.showConfiguredWindowIfPossible() { - return - } - do { - try await DashboardManager.shared.show() - } catch { - DashboardManager.shared.showFailure(error) - } - } + AppNavigationActions.openDashboard() } @MainActor @@ -250,6 +238,59 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private let webChatAutoLogger = Logger(subsystem: "ai.openclaw", category: "Chat") let updaterController: UpdaterProviding = makeUpdaterController() + func applicationDockMenu(_: NSApplication) -> NSMenu? { + let menu = NSMenu() + menu.autoenablesItems = false + menu.addItem(self.dockMenuItem( + title: "Open Dashboard", + systemImage: "gauge", + action: #selector(self.openDashboardFromDockMenu(_:)))) + menu.addItem(self.dockMenuItem( + title: "Open Chat", + systemImage: "bubble.left.and.bubble.right", + action: #selector(self.openChatFromDockMenu(_:)))) + let canvasTitle = AppStateStore.shared.canvasPanelVisible ? "Close Canvas" : "Open Canvas" + let canvasItem = self.dockMenuItem( + title: canvasTitle, + systemImage: "rectangle.inset.filled.on.rectangle", + action: #selector(self.toggleCanvasFromDockMenu(_:))) + canvasItem.isEnabled = AppStateStore.shared.canvasEnabled + menu.addItem(canvasItem) + menu.addItem(.separator()) + menu.addItem(self.dockMenuItem( + title: "Settings…", + systemImage: "gearshape", + action: #selector(self.openSettingsFromDockMenu(_:)))) + return menu + } + + private func dockMenuItem(title: String, systemImage: String, action: Selector) -> NSMenuItem { + let item = NSMenuItem(title: title, action: action, keyEquivalent: "") + item.target = self + item.image = NSImage(systemSymbolName: systemImage, accessibilityDescription: title) + return item + } + + @objc + private func openDashboardFromDockMenu(_: Any?) { + AppNavigationActions.openDashboard() + } + + @objc + private func openChatFromDockMenu(_: Any?) { + AppNavigationActions.openChat() + } + + @objc + private func toggleCanvasFromDockMenu(_: Any?) { + AppNavigationActions.toggleCanvas() + } + + @objc + private func openSettingsFromDockMenu(_: Any?) { + AppNavigationActions.openSettings() + } + func application(_: NSApplication, open urls: [URL]) { Task { @MainActor in for url in urls { diff --git a/apps/macos/Sources/OpenClaw/MenuContentView.swift b/apps/macos/Sources/OpenClaw/MenuContentView.swift index 7e0de41b17a..a19e9358fe0 100644 --- a/apps/macos/Sources/OpenClaw/MenuContentView.swift +++ b/apps/macos/Sources/OpenClaw/MenuContentView.swift @@ -111,28 +111,19 @@ struct MenuContent: View { self.voiceWakeMicMenu } Divider() - Link(destination: URL(string: "openclaw://dashboard")!) { + Button { + AppNavigationActions.openDashboard() + } label: { Label("Open Dashboard", systemImage: "gauge") } Button { - Task { @MainActor in - let sessionKey = await WebChatManager.shared.preferredSessionKey() - WebChatManager.shared.show(sessionKey: sessionKey) - } + AppNavigationActions.openChat() } label: { Label("Open Chat", systemImage: "bubble.left.and.bubble.right") } if self.state.canvasEnabled { Button { - Task { @MainActor in - if self.state.canvasPanelVisible { - CanvasManager.shared.hideAll() - } else { - let sessionKey = await GatewayConnection.shared.mainSessionKey() - // Don't force a navigation on re-open: preserve the current web view state. - _ = try? CanvasManager.shared.show(sessionKey: sessionKey, path: nil) - } - } + AppNavigationActions.toggleCanvas() } label: { Label( self.state.canvasPanelVisible ? "Close Canvas" : "Open Canvas", @@ -330,12 +321,7 @@ struct MenuContent: View { } private func open(tab: SettingsTab) { - SettingsTabRouter.request(tab) - NSApp.activate(ignoringOtherApps: true) - self.openSettings() - DispatchQueue.main.async { - NotificationCenter.default.post(name: .openclawSelectSettingsTab, object: tab) - } + AppNavigationActions.openSettings(tab: tab) } private var macNodeStatus: (label: String, color: Color)? { diff --git a/apps/macos/Tests/OpenClawIPCTests/MenuContentSmokeTests.swift b/apps/macos/Tests/OpenClawIPCTests/MenuContentSmokeTests.swift index cab820fe0e3..7a91fec25aa 100644 --- a/apps/macos/Tests/OpenClawIPCTests/MenuContentSmokeTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/MenuContentSmokeTests.swift @@ -1,3 +1,4 @@ +import AppKit import SwiftUI import Testing @testable import OpenClaw @@ -38,4 +39,15 @@ struct MenuContentSmokeTests { let view = MenuContent(state: state, updater: nil) _ = view.body } + + @Test func `dock menu exposes primary shortcuts`() throws { + let delegate = AppDelegate() + let menu = try #require(delegate.applicationDockMenu(NSApplication.shared)) + let titles = menu.items.map(\.title) + + #expect(titles.contains("Open Dashboard")) + #expect(titles.contains("Open Chat")) + #expect(titles.contains("Open Canvas") || titles.contains("Close Canvas")) + #expect(titles.contains("Settings…")) + } }