import AppKit import Foundation import Network import OSLog import WebKit private let webChatLogger = Logger(subsystem: "com.steipete.clawdis", category: "WebChat") final class WebChatWindowController: NSWindowController, WKNavigationDelegate { private let webView: WKWebView 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() config.userContentController = contentController config.preferences.isElementFullscreenEnabled = true config.preferences.setValue(true, forKey: "developerExtrasEnabled") self.webView = WKWebView(frame: .zero, configuration: config) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 960, height: 720), styleMask: [.titled, .closable, .resizable, .miniaturizable], backing: .buffered, defer: false) window.title = "Clawd Web Chat" window.contentView = self.webView super.init(window: window) self.webView.navigationDelegate = self self.loadPlaceholder() Task { await self.bootstrap() } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } private func loadPlaceholder() { let html = """
Connecting to web chat… """ self.webView.loadHTMLString(html, baseURL: nil) } private func loadPage(baseURL: URL) { self.webView.load(URLRequest(url: baseURL)) webChatLogger.debug("loadPage url=\(baseURL.absoluteString, privacy: .public)") } // MARK: - Bootstrap private func bootstrap() async { do { guard AppStateStore.webChatEnabled else { throw NSError(domain: "WebChat", code: 5, userInfo: [NSLocalizedDescriptionKey: "Web chat disabled in settings"]) } let endpoint = try await self.prepareEndpoint(remotePort: self.remotePort) self.baseEndpoint = endpoint await MainActor.run { var comps = URLComponents(url: endpoint.appendingPathComponent("webchat/"), resolvingAgainstBaseURL: false) comps?.queryItems = [URLQueryItem(name: "session", value: self.sessionKey)] if let url = comps?.url { self.loadPage(baseURL: url) } else { self.showError("invalid webchat url") } } } catch { let message = error.localizedDescription webChatLogger.error("webchat bootstrap failed: \(message, privacy: .public)") await MainActor.run { self.showError(message) } } } private func prepareEndpoint(remotePort: Int) async throws -> URL { if CommandResolver.connectionModeIsRemote() { 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 { @MainActor [weak self] in guard let self else { return } do { let base = try await self.startOrRestartTunnel() self.loadPage(baseURL: base) } catch { 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.