fix(mac): use gateway main session for WebChat
This commit is contained in:
@@ -60,9 +60,18 @@ final class DeepLinkHandler {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let channel = GatewayAgentChannel(raw: link.channel)
|
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(
|
let invocation = GatewayAgentInvocation(
|
||||||
message: messagePreview,
|
message: messagePreview,
|
||||||
sessionKey: link.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "main",
|
sessionKey: resolvedSessionKey,
|
||||||
thinking: link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty,
|
thinking: link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty,
|
||||||
deliver: channel.shouldDeliver(link.deliver),
|
deliver: channel.shouldDeliver(link.deliver),
|
||||||
to: link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty,
|
to: link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty,
|
||||||
|
|||||||
@@ -231,6 +231,38 @@ actor GatewayConnection {
|
|||||||
// MARK: - Typed gateway API
|
// MARK: - Typed gateway API
|
||||||
|
|
||||||
extension GatewayConnection {
|
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?) {
|
func status() async -> (ok: Bool, error: String?) {
|
||||||
do {
|
do {
|
||||||
_ = try await self.requestRaw(method: .status)
|
_ = try await self.requestRaw(method: .status)
|
||||||
|
|||||||
@@ -80,9 +80,10 @@ final class HoverHUDController {
|
|||||||
func openChat() {
|
func openChat() {
|
||||||
guard let anchorProvider = self.anchorProvider else { return }
|
guard let anchorProvider = self.anchorProvider else { return }
|
||||||
self.dismiss(reason: "openChat")
|
self.dismiss(reason: "openChat")
|
||||||
WebChatManager.shared.togglePanel(
|
Task { @MainActor in
|
||||||
sessionKey: WebChatManager.shared.preferredSessionKey(),
|
let sessionKey = await WebChatManager.shared.preferredSessionKey()
|
||||||
anchorProvider: anchorProvider)
|
WebChatManager.shared.togglePanel(sessionKey: sessionKey, anchorProvider: anchorProvider)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dismiss(reason: String = "explicit") {
|
func dismiss(reason: String = "explicit") {
|
||||||
|
|||||||
@@ -125,9 +125,12 @@ struct ClawdisApp: App {
|
|||||||
private func toggleWebChatPanel() {
|
private func toggleWebChatPanel() {
|
||||||
HoverHUDController.shared.setSuppressed(true)
|
HoverHUDController.shared.setSuppressed(true)
|
||||||
self.isMenuPresented = false
|
self.isMenuPresented = false
|
||||||
WebChatManager.shared.togglePanel(
|
Task { @MainActor in
|
||||||
sessionKey: WebChatManager.shared.preferredSessionKey(),
|
let sessionKey = await WebChatManager.shared.preferredSessionKey()
|
||||||
anchorProvider: { [self] in self.statusButtonScreenFrame() })
|
WebChatManager.shared.togglePanel(
|
||||||
|
sessionKey: sessionKey,
|
||||||
|
anchorProvider: { [self] in self.statusButtonScreenFrame() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -235,7 +238,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
// Developer/testing helper: auto-open chat when launched with --chat (or legacy --webchat).
|
// Developer/testing helper: auto-open chat when launched with --chat (or legacy --webchat).
|
||||||
if CommandLine.arguments.contains("--chat") || CommandLine.arguments.contains("--webchat") {
|
if CommandLine.arguments.contains("--chat") || CommandLine.arguments.contains("--webchat") {
|
||||||
self.webChatAutoLogger.debug("Auto-opening chat via CLI flag")
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ struct MenuContent: View {
|
|||||||
self.voiceWakeMicMenu
|
self.voiceWakeMicMenu
|
||||||
}
|
}
|
||||||
Button("Open Chat") {
|
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") {
|
Button("Open Dashboard") {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
|||||||
@@ -22,22 +22,31 @@ final class WebChatManager {
|
|||||||
static let shared = WebChatManager()
|
static let shared = WebChatManager()
|
||||||
|
|
||||||
private var windowController: WebChatSwiftUIWindowController?
|
private var windowController: WebChatSwiftUIWindowController?
|
||||||
|
private var windowSessionKey: String?
|
||||||
private var panelController: WebChatSwiftUIWindowController?
|
private var panelController: WebChatSwiftUIWindowController?
|
||||||
private var panelSessionKey: String?
|
private var panelSessionKey: String?
|
||||||
|
private var cachedPreferredSessionKey: String?
|
||||||
|
|
||||||
var onPanelVisibilityChanged: ((Bool) -> Void)?
|
var onPanelVisibilityChanged: ((Bool) -> Void)?
|
||||||
|
|
||||||
func show(sessionKey: String) {
|
func show(sessionKey: String) {
|
||||||
self.closePanel()
|
self.closePanel()
|
||||||
if let controller = self.windowController {
|
if let controller = self.windowController {
|
||||||
controller.show()
|
if self.windowSessionKey == sessionKey {
|
||||||
return
|
controller.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.close()
|
||||||
|
self.windowController = nil
|
||||||
|
self.windowSessionKey = nil
|
||||||
}
|
}
|
||||||
let controller = WebChatSwiftUIWindowController(sessionKey: sessionKey, presentation: .window)
|
let controller = WebChatSwiftUIWindowController(sessionKey: sessionKey, presentation: .window)
|
||||||
controller.onVisibilityChanged = { [weak self] visible in
|
controller.onVisibilityChanged = { [weak self] visible in
|
||||||
self?.onPanelVisibilityChanged?(visible)
|
self?.onPanelVisibilityChanged?(visible)
|
||||||
}
|
}
|
||||||
self.windowController = controller
|
self.windowController = controller
|
||||||
|
self.windowSessionKey = sessionKey
|
||||||
controller.show()
|
controller.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,26 +84,31 @@ final class WebChatManager {
|
|||||||
self.panelController?.close()
|
self.panelController?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func preferredSessionKey() -> String {
|
func preferredSessionKey() async -> String {
|
||||||
// The gateway store uses a canonical direct-chat bucket (default: "main").
|
if let cachedPreferredSessionKey { return cachedPreferredSessionKey }
|
||||||
// Avoid reading local session files; in remote mode they are not authoritative.
|
let key = await GatewayConnection.shared.mainSessionKey()
|
||||||
"main"
|
self.cachedPreferredSessionKey = key
|
||||||
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetTunnels() {
|
func resetTunnels() {
|
||||||
self.windowController?.close()
|
self.windowController?.close()
|
||||||
self.windowController = nil
|
self.windowController = nil
|
||||||
|
self.windowSessionKey = nil
|
||||||
self.panelController?.close()
|
self.panelController?.close()
|
||||||
self.panelController = nil
|
self.panelController = nil
|
||||||
self.panelSessionKey = nil
|
self.panelSessionKey = nil
|
||||||
|
self.cachedPreferredSessionKey = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func close() {
|
func close() {
|
||||||
self.windowController?.close()
|
self.windowController?.close()
|
||||||
self.windowController = nil
|
self.windowController = nil
|
||||||
|
self.windowSessionKey = nil
|
||||||
self.panelController?.close()
|
self.panelController?.close()
|
||||||
self.panelController = nil
|
self.panelController = nil
|
||||||
self.panelSessionKey = nil
|
self.panelSessionKey = nil
|
||||||
|
self.cachedPreferredSessionKey = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func panelHidden() {
|
private func panelHidden() {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import Testing
|
|||||||
@Suite(.serialized)
|
@Suite(.serialized)
|
||||||
@MainActor
|
@MainActor
|
||||||
struct WebChatManagerTests {
|
struct WebChatManagerTests {
|
||||||
@Test func preferredSessionKeyIsMain() {
|
@Test func preferredSessionKeyIsNonEmpty() async {
|
||||||
#expect(WebChatManager.shared.preferredSessionKey() == "main")
|
let key = await WebChatManager.shared.preferredSessionKey()
|
||||||
|
#expect(!key.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user