diff --git a/apps/macos/Sources/Clawdis/DeepLinks.swift b/apps/macos/Sources/Clawdis/DeepLinks.swift index 1706d9adb..d237907fb 100644 --- a/apps/macos/Sources/Clawdis/DeepLinks.swift +++ b/apps/macos/Sources/Clawdis/DeepLinks.swift @@ -60,9 +60,18 @@ final class DeepLinkHandler { do { let channel = GatewayAgentChannel(raw: link.channel) + let explicitSessionKey = link.sessionKey? + .trimmingCharacters(in: .whitespacesAndNewlines) + .nonEmpty + let resolvedSessionKey: String + if let explicitSessionKey { + resolvedSessionKey = explicitSessionKey + } else { + resolvedSessionKey = await GatewayConnection.shared.mainSessionKey() + } let invocation = GatewayAgentInvocation( message: messagePreview, - sessionKey: link.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "main", + sessionKey: resolvedSessionKey, thinking: link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty, deliver: channel.shouldDeliver(link.deliver), to: link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty, diff --git a/apps/macos/Sources/Clawdis/GatewayConnection.swift b/apps/macos/Sources/Clawdis/GatewayConnection.swift index 46ab20afa..8e1622bfe 100644 --- a/apps/macos/Sources/Clawdis/GatewayConnection.swift +++ b/apps/macos/Sources/Clawdis/GatewayConnection.swift @@ -231,6 +231,38 @@ actor GatewayConnection { // MARK: - Typed gateway API extension GatewayConnection { + struct ConfigGetSnapshot: Decodable, Sendable { + struct SnapshotConfig: Decodable, Sendable { + struct Inbound: Decodable, Sendable { + struct Session: Decodable, Sendable { + let mainKey: String? + } + + let session: Session? + } + + let inbound: Inbound? + } + + let config: SnapshotConfig? + } + + static func mainSessionKey(fromConfigGetData data: Data) throws -> String { + let snapshot = try JSONDecoder().decode(ConfigGetSnapshot.self, from: data) + let raw = snapshot.config?.inbound?.session?.mainKey + let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? "main" : trimmed + } + + func mainSessionKey(timeoutMs: Double = 15000) async -> String { + do { + let data = try await self.requestRaw(method: "config.get", params: nil, timeoutMs: timeoutMs) + return try Self.mainSessionKey(fromConfigGetData: data) + } catch { + return "main" + } + } + func status() async -> (ok: Bool, error: String?) { do { _ = try await self.requestRaw(method: .status) diff --git a/apps/macos/Sources/Clawdis/HoverHUD.swift b/apps/macos/Sources/Clawdis/HoverHUD.swift index db3a8db13..c2c636217 100644 --- a/apps/macos/Sources/Clawdis/HoverHUD.swift +++ b/apps/macos/Sources/Clawdis/HoverHUD.swift @@ -80,9 +80,10 @@ final class HoverHUDController { func openChat() { guard let anchorProvider = self.anchorProvider else { return } self.dismiss(reason: "openChat") - WebChatManager.shared.togglePanel( - sessionKey: WebChatManager.shared.preferredSessionKey(), - anchorProvider: anchorProvider) + Task { @MainActor in + let sessionKey = await WebChatManager.shared.preferredSessionKey() + WebChatManager.shared.togglePanel(sessionKey: sessionKey, anchorProvider: anchorProvider) + } } func dismiss(reason: String = "explicit") { diff --git a/apps/macos/Sources/Clawdis/MenuBar.swift b/apps/macos/Sources/Clawdis/MenuBar.swift index 0ccb1ce34..bfea2dddb 100644 --- a/apps/macos/Sources/Clawdis/MenuBar.swift +++ b/apps/macos/Sources/Clawdis/MenuBar.swift @@ -125,9 +125,12 @@ struct ClawdisApp: App { private func toggleWebChatPanel() { HoverHUDController.shared.setSuppressed(true) self.isMenuPresented = false - WebChatManager.shared.togglePanel( - sessionKey: WebChatManager.shared.preferredSessionKey(), - anchorProvider: { [self] in self.statusButtonScreenFrame() }) + Task { @MainActor in + let sessionKey = await WebChatManager.shared.preferredSessionKey() + WebChatManager.shared.togglePanel( + sessionKey: sessionKey, + anchorProvider: { [self] in self.statusButtonScreenFrame() }) + } } @MainActor @@ -235,7 +238,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { // Developer/testing helper: auto-open chat when launched with --chat (or legacy --webchat). if CommandLine.arguments.contains("--chat") || CommandLine.arguments.contains("--webchat") { self.webChatAutoLogger.debug("Auto-opening chat via CLI flag") - WebChatManager.shared.show(sessionKey: "main") + Task { @MainActor in + let sessionKey = await WebChatManager.shared.preferredSessionKey() + WebChatManager.shared.show(sessionKey: sessionKey) + } } } diff --git a/apps/macos/Sources/Clawdis/MenuContentView.swift b/apps/macos/Sources/Clawdis/MenuContentView.swift index e9c8f36c8..e9cd41642 100644 --- a/apps/macos/Sources/Clawdis/MenuContentView.swift +++ b/apps/macos/Sources/Clawdis/MenuContentView.swift @@ -42,7 +42,10 @@ struct MenuContent: View { self.voiceWakeMicMenu } Button("Open Chat") { - WebChatManager.shared.show(sessionKey: WebChatManager.shared.preferredSessionKey()) + Task { @MainActor in + let sessionKey = await WebChatManager.shared.preferredSessionKey() + WebChatManager.shared.show(sessionKey: sessionKey) + } } Button("Open Dashboard") { Task { @MainActor in diff --git a/apps/macos/Sources/Clawdis/WebChatManager.swift b/apps/macos/Sources/Clawdis/WebChatManager.swift index c4dc7ab94..3d550ada3 100644 --- a/apps/macos/Sources/Clawdis/WebChatManager.swift +++ b/apps/macos/Sources/Clawdis/WebChatManager.swift @@ -22,22 +22,31 @@ final class WebChatManager { static let shared = WebChatManager() private var windowController: WebChatSwiftUIWindowController? + private var windowSessionKey: String? private var panelController: WebChatSwiftUIWindowController? private var panelSessionKey: String? + private var cachedPreferredSessionKey: String? var onPanelVisibilityChanged: ((Bool) -> Void)? func show(sessionKey: String) { self.closePanel() if let controller = self.windowController { - controller.show() - return + if self.windowSessionKey == sessionKey { + controller.show() + return + } + + controller.close() + self.windowController = nil + self.windowSessionKey = nil } let controller = WebChatSwiftUIWindowController(sessionKey: sessionKey, presentation: .window) controller.onVisibilityChanged = { [weak self] visible in self?.onPanelVisibilityChanged?(visible) } self.windowController = controller + self.windowSessionKey = sessionKey controller.show() } @@ -75,26 +84,31 @@ final class WebChatManager { self.panelController?.close() } - func preferredSessionKey() -> String { - // The gateway store uses a canonical direct-chat bucket (default: "main"). - // Avoid reading local session files; in remote mode they are not authoritative. - "main" + func preferredSessionKey() async -> String { + if let cachedPreferredSessionKey { return cachedPreferredSessionKey } + let key = await GatewayConnection.shared.mainSessionKey() + self.cachedPreferredSessionKey = key + return key } func resetTunnels() { self.windowController?.close() self.windowController = nil + self.windowSessionKey = nil self.panelController?.close() self.panelController = nil self.panelSessionKey = nil + self.cachedPreferredSessionKey = nil } func close() { self.windowController?.close() self.windowController = nil + self.windowSessionKey = nil self.panelController?.close() self.panelController = nil self.panelSessionKey = nil + self.cachedPreferredSessionKey = nil } private func panelHidden() { diff --git a/apps/macos/Tests/ClawdisIPCTests/WebChatMainSessionKeyTests.swift b/apps/macos/Tests/ClawdisIPCTests/WebChatMainSessionKeyTests.swift new file mode 100644 index 000000000..ad0f58269 --- /dev/null +++ b/apps/macos/Tests/ClawdisIPCTests/WebChatMainSessionKeyTests.swift @@ -0,0 +1,57 @@ +import Foundation +import Testing +@testable import Clawdis + +@Suite struct WebChatMainSessionKeyTests { + @Test func configGetSnapshotMainKeyFallsBackToMainWhenMissing() throws { + let json = """ + { + "path": "/Users/pete/.clawdis/clawdis.json", + "exists": true, + "raw": null, + "parsed": {}, + "valid": true, + "config": {}, + "issues": [] + } + """ + let key = try GatewayConnection.mainSessionKey(fromConfigGetData: Data(json.utf8)) + #expect(key == "main") + } + + @Test func configGetSnapshotMainKeyTrimsAndUsesValue() throws { + let json = """ + { + "path": "/Users/pete/.clawdis/clawdis.json", + "exists": true, + "raw": null, + "parsed": {}, + "valid": true, + "config": { "inbound": { "session": { "mainKey": " primary " } } }, + "issues": [] + } + """ + let key = try GatewayConnection.mainSessionKey(fromConfigGetData: Data(json.utf8)) + #expect(key == "primary") + } + + @Test func configGetSnapshotMainKeyFallsBackWhenEmptyOrWhitespace() throws { + let json = """ + { + "config": { "inbound": { "session": { "mainKey": " " } } } + } + """ + let key = try GatewayConnection.mainSessionKey(fromConfigGetData: Data(json.utf8)) + #expect(key == "main") + } + + @Test func configGetSnapshotMainKeyFallsBackWhenConfigNull() throws { + let json = """ + { + "config": null + } + """ + let key = try GatewayConnection.mainSessionKey(fromConfigGetData: Data(json.utf8)) + #expect(key == "main") + } +} diff --git a/apps/macos/Tests/ClawdisIPCTests/WebChatManagerTests.swift b/apps/macos/Tests/ClawdisIPCTests/WebChatManagerTests.swift index a4f19634c..36ca826e9 100644 --- a/apps/macos/Tests/ClawdisIPCTests/WebChatManagerTests.swift +++ b/apps/macos/Tests/ClawdisIPCTests/WebChatManagerTests.swift @@ -4,7 +4,8 @@ import Testing @Suite(.serialized) @MainActor struct WebChatManagerTests { - @Test func preferredSessionKeyIsMain() { - #expect(WebChatManager.shared.preferredSessionKey() == "main") + @Test func preferredSessionKeyIsNonEmpty() async { + let key = await WebChatManager.shared.preferredSessionKey() + #expect(!key.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } }