diff --git a/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift b/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift index cf7e9e6e1..14aabc5ff 100644 --- a/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift +++ b/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift @@ -388,6 +388,43 @@ actor GatewayEndpointStore { } } +extension GatewayEndpointStore { + static func dashboardURL(for config: GatewayConnection.Config) throws -> URL { + guard var components = URLComponents(url: config.url, resolvingAgainstBaseURL: false) else { + throw NSError(domain: "Dashboard", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Invalid gateway URL", + ]) + } + switch components.scheme?.lowercased() { + case "ws": + components.scheme = "http" + case "wss": + components.scheme = "https" + default: + components.scheme = "http" + } + components.path = "/" + var queryItems: [URLQueryItem] = [] + if let token = config.token?.trimmingCharacters(in: .whitespacesAndNewlines), + !token.isEmpty + { + queryItems.append(URLQueryItem(name: "token", value: token)) + } + if let password = config.password?.trimmingCharacters(in: .whitespacesAndNewlines), + !password.isEmpty + { + queryItems.append(URLQueryItem(name: "password", value: password)) + } + components.queryItems = queryItems.isEmpty ? nil : queryItems + guard let url = components.url else { + throw NSError(domain: "Dashboard", code: 2, userInfo: [ + NSLocalizedDescriptionKey: "Failed to build dashboard URL", + ]) + } + return url + } +} + #if DEBUG extension GatewayEndpointStore { static func _testResolveGatewayPassword( diff --git a/apps/macos/Sources/Clawdbot/MenuContentView.swift b/apps/macos/Sources/Clawdbot/MenuContentView.swift index 87109bcf3..063008e66 100644 --- a/apps/macos/Sources/Clawdbot/MenuContentView.swift +++ b/apps/macos/Sources/Clawdbot/MenuContentView.swift @@ -313,34 +313,7 @@ struct MenuContent: View { private func openDashboard() async { do { let config = try await GatewayEndpointStore.shared.requireConfig() - let wsURL = config.url - guard var components = URLComponents(url: wsURL, resolvingAgainstBaseURL: false) else { - throw NSError(domain: "Dashboard", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Invalid gateway URL", - ]) - } - switch components.scheme?.lowercased() { - case "ws": - components.scheme = "http" - case "wss": - components.scheme = "https" - default: - components.scheme = "http" - } - components.path = "/" - var queryItems: [URLQueryItem] = [] - if let token = config.token, !token.isEmpty { - queryItems.append(URLQueryItem(name: "token", value: token)) - } - if let password = config.password, !password.isEmpty { - queryItems.append(URLQueryItem(name: "password", value: password)) - } - components.queryItems = queryItems.isEmpty ? nil : queryItems - guard let url = components.url else { - throw NSError(domain: "Dashboard", code: 2, userInfo: [ - NSLocalizedDescriptionKey: "Failed to build dashboard URL", - ]) - } + let url = try GatewayEndpointStore.dashboardURL(for: config) NSWorkspace.shared.open(url) } catch { let alert = NSAlert() diff --git a/ui/src/ui/gateway.ts b/ui/src/ui/gateway.ts index 9ce46c326..3fb884d46 100644 --- a/ui/src/ui/gateway.ts +++ b/ui/src/ui/gateway.ts @@ -50,6 +50,9 @@ export type GatewayBrowserClientOptions = { onGap?: (info: { expected: number; received: number }) => void; }; +// 4008 = application-defined code (browser rejects 1008 "Policy Violation") +const CONNECT_FAILED_CLOSE_CODE = 4008; + export class GatewayBrowserClient { private ws: WebSocket | null = null; private pending = new Map(); @@ -134,8 +137,7 @@ export class GatewayBrowserClient { this.opts.onHello?.(hello); }) .catch(() => { - // 4008 = application-defined code (browser rejects 1008 "Policy Violation") - this.ws?.close(4008, "connect failed"); + this.ws?.close(CONNECT_FAILED_CLOSE_CODE, "connect failed"); }); }