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? init(sessionKey: String) { webChatLogger.debug("init WebChatWindowController sessionKey=\(sessionKey, privacy: .public)") self.sessionKey = sessionKey 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: AppStateStore.webChatPort) 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() { 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)/")! } else { return URL(string: "http://127.0.0.1:\(remotePort)/")! } } private func showError(_ text: String) { let html = """ Web chat failed to connect.