From 70fb4d452e55e809d506593623e63d92810e439b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 10 Dec 2025 01:00:53 +0000 Subject: [PATCH] mac: tidy menu and gateway support --- .../Clawdis/ConnectionModeCoordinator.swift | 3 +- .../Sources/Clawdis/ControlChannel.swift | 12 +++- .../Sources/Clawdis/CritterStatusLabel.swift | 14 ++-- .../macos/Sources/Clawdis/DebugSettings.swift | 5 +- .../Sources/Clawdis/GatewayChannel.swift | 25 +++++-- .../Sources/Clawdis/GatewayEnvironment.swift | 5 +- .../Clawdis/GatewayProcessManager.swift | 5 +- .../Sources/Clawdis/GeneralSettings.swift | 9 ++- apps/macos/Sources/Clawdis/LogLocator.swift | 13 ++-- apps/macos/Sources/Clawdis/MenuBar.swift | 4 +- .../Sources/Clawdis/MenuContentView.swift | 68 +++++++++++-------- apps/macos/Sources/Clawdis/Onboarding.swift | 5 +- apps/macos/Sources/Clawdis/PortGuardian.swift | 54 +++++++++++---- .../Sources/Clawdis/RemoteTunnelManager.swift | 8 ++- .../macos/Sources/Clawdis/ToolsSettings.swift | 3 +- .../macos/Sources/Clawdis/WebChatWindow.swift | 59 ++++++++++++---- .../GatewayFrameDecodeTests.swift | 2 +- .../ClawdisIPCTests/InstancesStoreTests.swift | 2 +- 18 files changed, 198 insertions(+), 98 deletions(-) diff --git a/apps/macos/Sources/Clawdis/ConnectionModeCoordinator.swift b/apps/macos/Sources/Clawdis/ConnectionModeCoordinator.swift index b41fce1a2..4aac9d5f4 100644 --- a/apps/macos/Sources/Clawdis/ConnectionModeCoordinator.swift +++ b/apps/macos/Sources/Clawdis/ConnectionModeCoordinator.swift @@ -18,7 +18,8 @@ final class ConnectionModeCoordinator { try await ControlChannel.shared.configure(mode: .local) } catch { // Control channel will mark itself degraded; nothing else to do here. - self.logger.error("control channel local configure failed: \(error.localizedDescription, privacy: .public)") + self.logger.error( + "control channel local configure failed: \(error.localizedDescription, privacy: .public)") } if paused { GatewayProcessManager.shared.stop() diff --git a/apps/macos/Sources/Clawdis/ControlChannel.swift b/apps/macos/Sources/Clawdis/ControlChannel.swift index f335fa755..c11da2f56 100644 --- a/apps/macos/Sources/Clawdis/ControlChannel.swift +++ b/apps/macos/Sources/Clawdis/ControlChannel.swift @@ -147,9 +147,14 @@ final class ControlChannel: ObservableObject { // to free the port instead of a vague message. let nsError = error as NSError if nsError.domain == "Gateway", - nsError.localizedDescription.contains("hello failed (unexpected response)") { + nsError.localizedDescription.contains("hello failed (unexpected response)") + { let port = GatewayEnvironment.gatewayPort() - return "Gateway handshake got non-gateway data on localhost:\(port). Another process is using that port or the SSH forward failed. Stop the local gateway/port-forward on \(port) and retry Remote mode." + return """ + Gateway handshake got non-gateway data on localhost:\(port). + Another process is using that port or the SSH forward failed. + Stop the local gateway/port-forward on \(port) and retry Remote mode. + """ } if let urlError = error as? URLError { @@ -171,7 +176,8 @@ final class ControlChannel: ObservableObject { } if nsError.domain == "Gateway", nsError.code == 5 { - return "Gateway request timed out; check the gateway process on localhost:\(GatewayEnvironment.gatewayPort())." + let port = GatewayEnvironment.gatewayPort() + return "Gateway request timed out; check the gateway process on localhost:\(port)." } let detail = nsError.localizedDescription.isEmpty ? "unknown gateway error" : nsError.localizedDescription diff --git a/apps/macos/Sources/Clawdis/CritterStatusLabel.swift b/apps/macos/Sources/Clawdis/CritterStatusLabel.swift index 23b9502fa..a98732776 100644 --- a/apps/macos/Sources/Clawdis/CritterStatusLabel.swift +++ b/apps/macos/Sources/Clawdis/CritterStatusLabel.swift @@ -234,11 +234,11 @@ enum CritterIconRenderer { colorSpaceName: .deviceRGB, bitmapFormat: [], bytesPerRow: 0, - bitsPerPixel: 0 - ) else { - return NSImage(size: size) + bitsPerPixel: 0) + else { + return NSImage(size: self.size) } - rep.size = size + rep.size = self.size NSGraphicsContext.saveGraphicsState() if let context = NSGraphicsContext(bitmapImageRep: rep) { @@ -247,8 +247,8 @@ enum CritterIconRenderer { context.cgContext.setShouldAntialias(false) defer { NSGraphicsContext.restoreGraphicsState() } - let stepX = size.width / max(CGFloat(rep.pixelsWide), 1) - let stepY = size.height / max(CGFloat(rep.pixelsHigh), 1) + let stepX = self.size.width / max(CGFloat(rep.pixelsWide), 1) + let stepY = self.size.height / max(CGFloat(rep.pixelsHigh), 1) let snapX: (CGFloat) -> CGFloat = { ($0 / stepX).rounded() * stepX } let snapY: (CGFloat) -> CGFloat = { ($0 / stepY).rounded() * stepY } @@ -373,7 +373,7 @@ enum CritterIconRenderer { context.cgContext.restoreGState() } else { NSGraphicsContext.restoreGraphicsState() - return NSImage(size: size) + return NSImage(size: self.size) } let image = NSImage(size: size) diff --git a/apps/macos/Sources/Clawdis/DebugSettings.swift b/apps/macos/Sources/Clawdis/DebugSettings.swift index 3c925fb74..e63ec85fd 100644 --- a/apps/macos/Sources/Clawdis/DebugSettings.swift +++ b/apps/macos/Sources/Clawdis/DebugSettings.swift @@ -94,7 +94,7 @@ struct DebugSettings: View { .font(.caption2) .foregroundStyle(.secondary) } - if self.portReports.isEmpty && !self.portCheckInFlight { + if self.portReports.isEmpty, !self.portCheckInFlight { Text("Check which process owns 18788/18789 and suggest fixes.") .font(.caption2) .foregroundStyle(.secondary) @@ -281,8 +281,7 @@ struct DebugSettings: View { primaryButton: .destructive(Text("Kill")) { Task { await self.killConfirmed(listener.pid) } }, - secondaryButton: .cancel() - ) + secondaryButton: .cancel()) } } diff --git a/apps/macos/Sources/Clawdis/GatewayChannel.swift b/apps/macos/Sources/Clawdis/GatewayChannel.swift index 1bc8a8034..53e76e31e 100644 --- a/apps/macos/Sources/Clawdis/GatewayChannel.swift +++ b/apps/macos/Sources/Clawdis/GatewayChannel.swift @@ -34,7 +34,7 @@ private actor GatewayChannelActor { private let decoder = JSONDecoder() private let encoder = JSONEncoder() private var watchdogTask: Task? - private let defaultRequestTimeoutMs: Double = 15_000 + private let defaultRequestTimeoutMs: Double = 15000 init(url: URL, token: String?) { self.url = url @@ -94,7 +94,8 @@ private actor GatewayChannelActor { maxprotocol: GATEWAY_PROTOCOL_VERSION, client: [ "name": ClawdisProtocol.AnyCodable("clawdis-mac"), - "version": ClawdisProtocol.AnyCodable(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"), + "version": ClawdisProtocol.AnyCodable( + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"), "platform": ClawdisProtocol.AnyCodable(platform), "mode": ClawdisProtocol.AnyCodable("app"), "instanceId": ClawdisProtocol.AnyCodable(Host.current().localizedName ?? UUID().uuidString), @@ -106,7 +107,10 @@ private actor GatewayChannelActor { let data = try JSONEncoder().encode(hello) try await self.task?.send(.data(data)) guard let msg = try await task?.receive() else { - throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "hello failed (no response)"]) + throw NSError( + domain: "Gateway", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "hello failed (no response)"]) } try await self.handleHelloResponse(msg) } @@ -118,7 +122,10 @@ private actor GatewayChannelActor { @unknown default: nil } guard let data else { - throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "hello failed (empty response)"]) + throw NSError( + domain: "Gateway", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "hello failed (empty response)"]) } let decoder = JSONDecoder() if let ok = try? decoder.decode(HelloOk.self, from: data) { @@ -134,9 +141,15 @@ private actor GatewayChannelActor { return } if let err = try? decoder.decode(HelloError.self, from: data) { - throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "hello-error: \(err.reason)"]) + throw NSError( + domain: "Gateway", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "hello-error: \(err.reason)"]) } - throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "hello failed (unexpected response)"]) + throw NSError( + domain: "Gateway", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "hello failed (unexpected response)"]) } private func listen() { diff --git a/apps/macos/Sources/Clawdis/GatewayEnvironment.swift b/apps/macos/Sources/Clawdis/GatewayEnvironment.swift index 4d6ff1fce..d4e6d9be2 100644 --- a/apps/macos/Sources/Clawdis/GatewayEnvironment.swift +++ b/apps/macos/Sources/Clawdis/GatewayEnvironment.swift @@ -105,7 +105,10 @@ enum GatewayEnvironment { nodeVersion: runtime.version.description, gatewayVersion: installed.description, requiredGateway: expected.description, - message: "Gateway version \(installed.description) is incompatible with app \(expected.description); install/update the global package.") + message: """ + Gateway version \(installed.description) is incompatible with app \(expected.description); + install or update the global package. + """) } let gatewayLabel = gatewayBin != nil ? "global" : "local" diff --git a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift index 1e5ea2a84..287ec0bb5 100644 --- a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift +++ b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift @@ -1,7 +1,7 @@ import Foundation +import Network import OSLog import Subprocess -import Network #if canImport(Darwin) import Darwin #endif @@ -229,8 +229,7 @@ final class GatewayProcessManager: ObservableObject { FilePath(path), .readWrite, options: [.create, .exclusiveCreate], - permissions: [.ownerReadWrite] - ) + permissions: [.ownerReadWrite]) return GatewayLockHandle(fd: fd, path: path) } diff --git a/apps/macos/Sources/Clawdis/GeneralSettings.swift b/apps/macos/Sources/Clawdis/GeneralSettings.swift index b999bf4f1..1b3ff90e1 100644 --- a/apps/macos/Sources/Clawdis/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdis/GeneralSettings.swift @@ -525,13 +525,18 @@ extension GeneralSettings { let target = LogLocator.bestLogFile() if let target { - NSWorkspace.shared.selectFile(target.path, inFileViewerRootedAtPath: target.deletingLastPathComponent().path) + NSWorkspace.shared.selectFile( + target.path, + inFileViewerRootedAtPath: target.deletingLastPathComponent().path) return } let alert = NSAlert() alert.messageText = "Log file not found" - alert.informativeText = "Looked for clawdis logs in /tmp/clawdis/. Run a health check or send a message to generate activity, then try again." + alert.informativeText = """ + Looked for clawdis logs in /tmp/clawdis/. + Run a health check or send a message to generate activity, then try again. + """ alert.alertStyle = .informational alert.addButton(withTitle: "OK") alert.runModal() diff --git a/apps/macos/Sources/Clawdis/LogLocator.swift b/apps/macos/Sources/Clawdis/LogLocator.swift index f437b8176..75ebb6e76 100644 --- a/apps/macos/Sources/Clawdis/LogLocator.swift +++ b/apps/macos/Sources/Clawdis/LogLocator.swift @@ -4,22 +4,23 @@ enum LogLocator { private static let logDir = URL(fileURLWithPath: "/tmp/clawdis") private static let stdoutLog = logDir.appendingPathComponent("clawdis-stdout.log") + private static func modificationDate(for url: URL) -> Date { + (try? url.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast + } + /// Returns the newest log file under /tmp/clawdis/ (rolling or stdout), or nil if none exist. static func bestLogFile() -> URL? { let fm = FileManager.default let files = (try? fm.contentsOfDirectory( - at: logDir, + at: self.logDir, includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles])) ?? [] return files .filter { $0.lastPathComponent.hasPrefix("clawdis") && $0.pathExtension == "log" } - .sorted { lhs, rhs in - let lDate = (try? lhs.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast - let rDate = (try? rhs.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast - return lDate > rDate + .max { lhs, rhs in + self.modificationDate(for: lhs) < self.modificationDate(for: rhs) } - .first } /// Path to use for launchd stdout/err. diff --git a/apps/macos/Sources/Clawdis/MenuBar.swift b/apps/macos/Sources/Clawdis/MenuBar.swift index 68a95cfe2..3791e5b1c 100644 --- a/apps/macos/Sources/Clawdis/MenuBar.swift +++ b/apps/macos/Sources/Clawdis/MenuBar.swift @@ -15,7 +15,7 @@ struct ClawdisApp: App { @State private var statusItem: NSStatusItem? @State private var isMenuPresented = false @State private var isPanelVisible = false - + @MainActor private func updateStatusHighlight() { self.statusItem?.button?.highlight(self.isPanelVisible) @@ -94,7 +94,7 @@ struct ClawdisApp: App { handler.leadingAnchor.constraint(equalTo: button.leadingAnchor), handler.trailingAnchor.constraint(equalTo: button.trailingAnchor), handler.topAnchor.constraint(equalTo: button.topAnchor), - handler.bottomAnchor.constraint(equalTo: button.bottomAnchor) + handler.bottomAnchor.constraint(equalTo: button.bottomAnchor), ]) } diff --git a/apps/macos/Sources/Clawdis/MenuContentView.swift b/apps/macos/Sources/Clawdis/MenuContentView.swift index 961041c65..a3b78cab3 100644 --- a/apps/macos/Sources/Clawdis/MenuContentView.swift +++ b/apps/macos/Sources/Clawdis/MenuContentView.swift @@ -33,7 +33,9 @@ struct MenuContent: View { self.voiceWakeMicMenu } if AppStateStore.webChatEnabled { - Button("Open Chat") { WebChatManager.shared.show(sessionKey: WebChatManager.shared.preferredSessionKey()) } + Button("Open Chat") { + WebChatManager.shared.show(sessionKey: WebChatManager.shared.preferredSessionKey()) + } } Divider() Button("Settings…") { self.open(tab: .general) } @@ -59,7 +61,9 @@ struct MenuContent: View { await self.reloadSessionMenu() } } label: { - Label(level.capitalized, systemImage: row.thinkingLevel == normalized ? "checkmark" : "") + Label( + level.capitalized, + systemImage: row.thinkingLevel == normalized ? "checkmark" : "") } } } @@ -75,7 +79,9 @@ struct MenuContent: View { await self.reloadSessionMenu() } } label: { - Label(level.capitalized, systemImage: row.verboseLevel == normalized ? "checkmark" : "") + Label( + level.capitalized, + systemImage: row.verboseLevel == normalized ? "checkmark" : "") } } } @@ -216,19 +222,21 @@ struct MenuContent: View { } }() - return Button(action: {}) { - HStack(spacing: 8) { - Circle() - .fill(color) - .frame(width: 8, height: 8) - Text(label) - .font(.caption.weight(.semibold)) - .foregroundStyle(.primary) - } - .padding(.vertical, 4) - } - .buttonStyle(.plain) - .disabled(true) + return Button( + action: {}, + label: { + HStack(spacing: 8) { + Circle() + .fill(color) + .frame(width: 8, height: 8) + Text(label) + .font(.caption.weight(.semibold)) + .foregroundStyle(.primary) + } + .padding(.vertical, 4) + }) + .buttonStyle(.plain) + .disabled(true) } private var heartbeatStatusRow: some View { @@ -254,19 +262,21 @@ struct MenuContent: View { } }() - return Button(action: {}) { - HStack(spacing: 8) { - Circle() - .fill(color) - .frame(width: 8, height: 8) - Text(label) - .font(.caption.weight(.semibold)) - .foregroundStyle(.primary) - } - .padding(.vertical, 2) - } - .buttonStyle(.plain) - .disabled(true) + return Button( + action: {}, + label: { + HStack(spacing: 8) { + Circle() + .fill(color) + .frame(width: 8, height: 8) + Text(label) + .font(.caption.weight(.semibold)) + .foregroundStyle(.primary) + } + .padding(.vertical, 2) + }) + .buttonStyle(.plain) + .disabled(true) } private var activeBinding: Binding { diff --git a/apps/macos/Sources/Clawdis/Onboarding.swift b/apps/macos/Sources/Clawdis/Onboarding.swift index 9271d59c3..c0b7532f5 100644 --- a/apps/macos/Sources/Clawdis/Onboarding.swift +++ b/apps/macos/Sources/Clawdis/Onboarding.swift @@ -182,7 +182,10 @@ struct OnboardingView: View { Text("Install the gateway") .font(.largeTitle.weight(.semibold)) Text( - "Clawdis now runs the WebSocket gateway from the global \"clawdis\" package. Install/update it here and we’ll check Node for you.") + """ + Clawdis now runs the WebSocket gateway from the global "clawdis" package. + Install/update it here and we’ll check Node for you. + """) .font(.body) .foregroundStyle(.secondary) .multilineTextAlignment(.center) diff --git a/apps/macos/Sources/Clawdis/PortGuardian.swift b/apps/macos/Sources/Clawdis/PortGuardian.swift index 9254e2961..5a2fd7943 100644 --- a/apps/macos/Sources/Clawdis/PortGuardian.swift +++ b/apps/macos/Sources/Clawdis/PortGuardian.swift @@ -23,11 +23,12 @@ actor PortGuardian { private var records: [Record] = [] private let logger = Logger(subsystem: "com.steipete.clawdis", category: "portguard") - nonisolated private static let appSupportDir: URL = { + private nonisolated static let appSupportDir: URL = { let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! return base.appendingPathComponent("Clawdis", isDirectory: true) }() - nonisolated private static var recordPath: URL { + + private nonisolated static var recordPath: URL { self.appSupportDir.appendingPathComponent("port-guard.json", isDirectory: false) } @@ -43,12 +44,20 @@ actor PortGuardian { guard !listeners.isEmpty else { continue } for listener in listeners { if self.isExpected(listener, port: port, mode: mode) { - self.logger.info("port \(port, privacy: .public) already served by expected \(listener.command, privacy: .public) (pid \(listener.pid)) — keeping") + let message = """ + port \(port) already served by expected \(listener.command) + (pid \(listener.pid)) — keeping + """ + self.logger.info("\(message, privacy: .public)") continue } let killed = await self.kill(listener.pid) if killed { - self.logger.error("port \(port, privacy: .public) was held by \(listener.command, privacy: .public) (pid \(listener.pid)); terminated") + let message = """ + port \(port) was held by \(listener.command) + (pid \(listener.pid)); terminated + """ + self.logger.error("\(message, privacy: .public)") } else { self.logger.error("failed to terminate pid \(listener.pid) on port \(port, privacy: .public)") } @@ -60,7 +69,13 @@ actor PortGuardian { func record(port: Int, pid: Int32, command: String, mode: AppState.ConnectionMode) async { try? FileManager.default.createDirectory(at: Self.appSupportDir, withIntermediateDirectories: true) self.records.removeAll { $0.pid == pid } - self.records.append(Record(port: port, pid: pid, command: command, mode: mode.rawValue, timestamp: Date().timeIntervalSince1970)) + self.records.append( + Record( + port: port, + pid: pid, + command: command, + mode: mode.rawValue, + timestamp: Date().timeIntervalSince1970)) self.save() } @@ -93,9 +108,9 @@ actor PortGuardian { var summary: String { switch self.status { - case let .ok(text): return text - case let .missing(text): return text - case let .interference(text, _): return text + case let .ok(text): text + case let .missing(text): text + case let .interference(text, _): text } } } @@ -105,6 +120,7 @@ actor PortGuardian { let path = Self.executablePath(for: listener.pid) return Descriptor(pid: listener.pid, command: listener.command, executablePath: path) } + // MARK: - Internals private struct Listener { @@ -132,6 +148,7 @@ actor PortGuardian { let listeners = await self.listeners(on: port) let expectedDesc: String let okPredicate: (Listener) -> Bool + let expectedCommands = ["node", "clawdis", "tsx", "pnpm", "bun"] switch mode { case .remote: @@ -143,7 +160,7 @@ actor PortGuardian { : "Gateway websocket (node/tsx)" okPredicate = { listener in let c = listener.command.lowercased() - return c.contains("node") || c.contains("clawdis") || c.contains("tsx") || c.contains("pnpm") || c.contains("bun") + return expectedCommands.contains { c.contains($0) } } } @@ -166,11 +183,19 @@ actor PortGuardian { if offenders.isEmpty { let list = listeners.map { "\($0.command) (\($0.pid))" }.joined(separator: ", ") let okText = "Port \(port) is served by \(list)." - reports.append(.init(port: port, expected: expectedDesc, status: .ok(okText), listeners: reportListeners)) + reports.append(.init( + port: port, + expected: expectedDesc, + status: .ok(okText), + listeners: reportListeners)) } else { let list = offenders.map { "\($0.command) (\($0.pid))" }.joined(separator: ", ") let reason = "Port \(port) is held by \(list), expected \(expectedDesc)." - reports.append(.init(port: port, expected: expectedDesc, status: .interference(reason, offenders: offenders), listeners: reportListeners)) + reports.append(.init( + port: port, + expected: expectedDesc, + status: .interference(reason, offenders: offenders), + listeners: reportListeners)) } } @@ -245,11 +270,13 @@ actor PortGuardian { guard length > 0 else { return nil } // Drop trailing null and decode as UTF-8. let trimmed = buffer.prefix { $0 != 0 } - return String(decoding: trimmed.map { UInt8(bitPattern: $0) }, as: UTF8.self) + let bytes = trimmed.map { UInt8(bitPattern: $0) } + return String(bytes: bytes, encoding: .utf8) #else return nil #endif } + private func kill(_ pid: Int32) async -> Bool { let term = await ShellExecutor.run(command: ["kill", "-TERM", "\(pid)"], cwd: nil, env: nil, timeout: 2) if term.ok { return true } @@ -259,6 +286,7 @@ actor PortGuardian { private func isExpected(_ listener: Listener, port: Int, mode: AppState.ConnectionMode) -> Bool { let cmd = listener.command.lowercased() + let expectedCommands = ["node", "clawdis", "tsx", "pnpm", "bun"] switch mode { case .remote: if port == 18788 { @@ -266,7 +294,7 @@ actor PortGuardian { } return false case .local: - return cmd.contains("node") || cmd.contains("clawdis") || cmd.contains("tsx") || cmd.contains("pnpm") || cmd.contains("bun") + return expectedCommands.contains { cmd.contains($0) } } } diff --git a/apps/macos/Sources/Clawdis/RemoteTunnelManager.swift b/apps/macos/Sources/Clawdis/RemoteTunnelManager.swift index 4a65ed4c2..e1a6b7423 100644 --- a/apps/macos/Sources/Clawdis/RemoteTunnelManager.swift +++ b/apps/macos/Sources/Clawdis/RemoteTunnelManager.swift @@ -11,12 +11,16 @@ actor RemoteTunnelManager { func ensureControlTunnel() async throws -> UInt16 { let settings = CommandResolver.connectionSettings() guard settings.mode == .remote else { - throw NSError(domain: "RemoteTunnel", code: 1, userInfo: [NSLocalizedDescriptionKey: "Remote mode is not enabled"]) + throw NSError( + domain: "RemoteTunnel", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Remote mode is not enabled"]) } if let tunnel = self.controlTunnel, tunnel.process.isRunning, - let local = tunnel.localPort { + let local = tunnel.localPort + { return local } diff --git a/apps/macos/Sources/Clawdis/ToolsSettings.swift b/apps/macos/Sources/Clawdis/ToolsSettings.swift index e826d4cfb..936c45e24 100644 --- a/apps/macos/Sources/Clawdis/ToolsSettings.swift +++ b/apps/macos/Sources/Clawdis/ToolsSettings.swift @@ -271,8 +271,7 @@ struct ToolsSettings: View { let current = self.installStates[tool.id] ?? .checking return Binding( get: { self.installStates[tool.id] ?? current }, - set: { self.installStates[tool.id] = $0 } - ) + set: { self.installStates[tool.id] = $0 }) } private func refreshAll() { diff --git a/apps/macos/Sources/Clawdis/WebChatWindow.swift b/apps/macos/Sources/Clawdis/WebChatWindow.swift index 4ca97c40f..990c50f22 100644 --- a/apps/macos/Sources/Clawdis/WebChatWindow.swift +++ b/apps/macos/Sources/Clawdis/WebChatWindow.swift @@ -123,7 +123,18 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N private func loadPlaceholder() { let html = """ - Connecting to web chat… + + + Connecting to web chat… + + """ self.webView.loadHTMLString(html, baseURL: nil) } @@ -165,9 +176,9 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N private func prepareEndpoint(remotePort: Int) async throws -> URL { if CommandResolver.connectionModeIsRemote() { - return try await self.startOrRestartTunnel() + try await self.startOrRestartTunnel() } else { - return URL(string: "http://127.0.0.1:\(remotePort)/")! + URL(string: "http://127.0.0.1:\(remotePort)/")! } } @@ -208,7 +219,10 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N private func isWebChatBooted() async -> Bool { await withCheckedContinuation { cont in - self.webView.evaluateJavaScript("document.getElementById('app')?.dataset.booted === '1' || document.body.dataset.webchatError === '1'") { result, _ in + self.webView.evaluateJavaScript(""" + document.getElementById('app')?.dataset.booted === '1' || + document.body.dataset.webchatError === '1' + """) { result, _ in cont.resume(returning: result as? Bool ?? false) } } @@ -306,8 +320,18 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N private func showError(_ text: String) { self.bootWatchTask?.cancel() let html = """ - Web chat failed to connect.

\( - text) + + + Web chat failed to connect.

\(text) + + """ self.webView.loadHTMLString(html, baseURL: nil) } @@ -354,8 +378,8 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N private func installDismissMonitor() { guard self.localDismissMonitor == nil, let panel = self.window else { return } self.localDismissMonitor = NSEvent.addGlobalMonitorForEvents( - matching: [.leftMouseDown, .rightMouseDown, .otherMouseDown] - ) { [weak self] _ in + matching: [.leftMouseDown, .rightMouseDown, .otherMouseDown]) + { [weak self] _ in guard let self else { return } let pt = NSEvent.mouseLocation // screen coordinates if !panel.frame.contains(pt) { @@ -379,8 +403,8 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N let o1 = nc.addObserver( forName: NSApplication.didResignActiveNotification, object: nil, - queue: .main - ) { [weak self] _ in + queue: .main) + { [weak self] _ in Task { @MainActor in self?.closePanel() } @@ -388,8 +412,8 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N let o2 = nc.addObserver( forName: NSWindow.didChangeOcclusionStateNotification, object: window, - queue: .main - ) { [weak self] _ in + queue: .main) + { [weak self] _ in Task { @MainActor in guard let self, case .panel = self.presentation else { return } if !(window.occlusionState.contains(.visible)) { @@ -402,7 +426,9 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N private func removePanelObservers() { let nc = NotificationCenter.default - for o in self.observers { nc.removeObserver(o) } + for o in self.observers { + nc.removeObserver(o) + } self.observers.removeAll() } } @@ -431,7 +457,7 @@ extension WebChatWindowController { { let end = hostname.firstIndex(of: 0) ?? hostname.count let bytes = hostname[..