diff --git a/apps/macos/Sources/Clawdis/WebChatWindow.swift b/apps/macos/Sources/Clawdis/WebChatWindow.swift index 3932b7728..1b5c82633 100644 --- a/apps/macos/Sources/Clawdis/WebChatWindow.swift +++ b/apps/macos/Sources/Clawdis/WebChatWindow.swift @@ -11,10 +11,12 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { private let sessionKey: String private var tunnel: WebChatTunnel? private var baseEndpoint: URL? + private let remotePort: Int init(sessionKey: String) { webChatLogger.debug("init WebChatWindowController sessionKey=\(sessionKey, privacy: .public)") self.sessionKey = sessionKey + self.remotePort = AppStateStore.webChatPort let config = WKWebViewConfiguration() let contentController = WKUserContentController() @@ -59,7 +61,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { guard AppStateStore.webChatEnabled else { throw NSError(domain: "WebChat", code: 5, userInfo: [NSLocalizedDescriptionKey: "Web chat disabled in settings"]) } - let endpoint = try await self.prepareEndpoint(remotePort: AppStateStore.webChatPort) + let endpoint = try await self.prepareEndpoint(remotePort: self.remotePort) self.baseEndpoint = endpoint await MainActor.run { var comps = URLComponents(url: endpoint.appendingPathComponent("webchat/"), resolvingAgainstBaseURL: false) @@ -79,17 +81,42 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { private func prepareEndpoint(remotePort: Int) async throws -> URL { if CommandResolver.connectionModeIsRemote() { - let tunnel = try await WebChatTunnel.create(remotePort: remotePort) - self.tunnel = tunnel - guard let port = tunnel.localPort else { - throw NSError(domain: "WebChat", code: 2, userInfo: [NSLocalizedDescriptionKey: "tunnel missing port"]) - } - return URL(string: "http://127.0.0.1:\(port)/")! + return try await self.startOrRestartTunnel() } else { return URL(string: "http://127.0.0.1:\(remotePort)/")! } } + private func startOrRestartTunnel() async throws -> URL { + // Kill existing tunnel if any + self.tunnel?.terminate() + + let tunnel = try await WebChatTunnel.create(remotePort: self.remotePort, preferredLocalPort: 18_788) + self.tunnel = tunnel + + // Auto-restart on unexpected termination while window lives + tunnel.process.terminationHandler = { [weak self] _ in + guard let self else { return } + webChatLogger.error("webchat tunnel terminated; restarting") + Task { [weak self] in + guard let self else { return } + do { + _ = try await self.startOrRestartTunnel() + if let base = self.baseEndpoint { + await MainActor.run { self.loadPage(baseURL: base) } + } + } catch { + await MainActor.run { self.showError(error.localizedDescription) } + } + } + } + + guard let port = tunnel.localPort else { + throw NSError(domain: "WebChat", code: 2, userInfo: [NSLocalizedDescriptionKey: "tunnel missing port"]) + } + return URL(string: "http://127.0.0.1:\(port)/")! + } + private func showError(_ text: String) { let html = """
Web chat failed to connect.