diff --git a/apps/macos/Sources/Clawdis/Resources/WebChat/bootstrap.js b/apps/macos/Sources/Clawdis/Resources/WebChat/bootstrap.js index 70b032db4..6bf2a3abe 100644 --- a/apps/macos/Sources/Clawdis/Resources/WebChat/bootstrap.js +++ b/apps/macos/Sources/Clawdis/Resources/WebChat/bootstrap.js @@ -264,6 +264,7 @@ const startChat = async () => { startChat().catch((err) => { const msg = err?.stack || err?.message || String(err); logStatus(`boot failed: ${msg}`); + document.body.dataset.webchatError = "1"; document.body.style.color = "#e06666"; document.body.style.fontFamily = "monospace"; document.body.style.padding = "16px"; diff --git a/apps/macos/Sources/Clawdis/Resources/WebChat/webchat.bundle.js b/apps/macos/Sources/Clawdis/Resources/WebChat/webchat.bundle.js index dab5d34cc..02f61448a 100644 --- a/apps/macos/Sources/Clawdis/Resources/WebChat/webchat.bundle.js +++ b/apps/macos/Sources/Clawdis/Resources/WebChat/webchat.bundle.js @@ -131248,11 +131248,14 @@ var init_katex = __esmMin((() => { case "\\htmlData": { var data = value.split(","); for (var i$7 = 0; i$7 < data.length; i$7++) { - var keyVal = data[i$7].split("="); - if (keyVal.length !== 2) { - throw new ParseError("Error parsing key-value for \\htmlData"); + var item = data[i$7]; + var firstEquals = item.indexOf("="); + if (firstEquals < 0) { + throw new ParseError("\\htmlData key/value '" + item + "'" + " missing equals sign"); } - attributes["data-" + keyVal[0].trim()] = keyVal[1].trim(); + var key = item.slice(0, firstEquals); + var _value = item.slice(firstEquals + 1); + attributes["data-" + key.trim()] = _value; } trustContext = { command: "\\htmlData", @@ -135726,7 +135729,7 @@ var init_katex = __esmMin((() => { return renderError(error$2, expression, settings); } }; - version$2 = "0.16.26"; + version$2 = "0.16.27"; __domTree = { Span, Anchor, @@ -196565,10 +196568,11 @@ const startChat = async () => { startChat().catch((err) => { const msg = err?.stack || err?.message || String(err); logStatus(`boot failed: ${msg}`); + document.body.dataset.webchatError = "1"; document.body.style.color = "#e06666"; document.body.style.fontFamily = "monospace"; document.body.style.padding = "16px"; document.body.innerText = "Web chat failed to load:\\n" + msg; }); -//#endregion \ No newline at end of file +//#endregion diff --git a/apps/macos/Sources/Clawdis/WebChatWindow.swift b/apps/macos/Sources/Clawdis/WebChatWindow.swift index ba29d56d1..262d59d99 100644 --- a/apps/macos/Sources/Clawdis/WebChatWindow.swift +++ b/apps/macos/Sources/Clawdis/WebChatWindow.swift @@ -25,6 +25,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N private let remotePort: Int private var reachabilityTask: Task? private var tunnelRestartEnabled = false + private var bootWatchTask: Task? let presentation: WebChatPresentation var onPanelClosed: (() -> Void)? private var panelCloseNotified = false @@ -56,10 +57,12 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N @MainActor deinit { self.reachabilityTask?.cancel() + self.bootWatchTask?.cancel() self.stopTunnel(allowRestart: false) } private static func makeWindow(for presentation: WebChatPresentation, contentView: NSView) -> NSWindow { + let wrappedContent = Self.makeRoundedContainer(containing: contentView) switch presentation { case .window: let window = NSWindow( @@ -68,7 +71,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N backing: .buffered, defer: false) window.title = "Clawd Web Chat" - window.contentView = contentView + window.contentView = wrappedContent return window case .panel: let panel = NSPanel( @@ -83,14 +86,32 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] panel.titleVisibility = .hidden panel.titlebarAppearsTransparent = true - panel.backgroundColor = .windowBackgroundColor + panel.backgroundColor = .clear panel.isOpaque = false - panel.contentView = contentView + panel.contentView = wrappedContent panel.becomesKeyOnlyIfNeeded = true return panel } } + private static func makeRoundedContainer(containing contentView: NSView) -> NSView { + let container = NSView(frame: .zero) + container.wantsLayer = true + container.layer?.cornerRadius = 12 + container.layer?.masksToBounds = true + container.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor + + contentView.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(contentView) + NSLayoutConstraint.activate([ + contentView.leadingAnchor.constraint(equalTo: container.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: container.trailingAnchor), + contentView.topAnchor.constraint(equalTo: container.topAnchor), + contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor), + ]) + return container + } + private func loadPlaceholder() { let html = """ Connecting to web chat… @@ -100,6 +121,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N private func loadPage(baseURL: URL) { self.webView.load(URLRequest(url: baseURL)) + self.startBootWatch() webChatLogger.debug("loadPage url=\(baseURL.absoluteString, privacy: .public)") } @@ -134,9 +156,9 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N private func prepareEndpoint(remotePort: Int) async throws -> URL { if CommandResolver.connectionModeIsRemote() { - try await self.startOrRestartTunnel() + return try await self.startOrRestartTunnel() } else { - URL(string: "http://127.0.0.1:\(remotePort)/")! + return URL(string: "http://127.0.0.1:\(remotePort)/")! } } @@ -157,6 +179,29 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N self.loadPage(baseURL: url) } + private func startBootWatch() { + self.bootWatchTask?.cancel() + self.bootWatchTask = Task { [weak self] in + guard let self else { return } + for _ in 0..<12 { + try? await Task.sleep(nanoseconds: 1_000_000_000) + if Task.isCancelled { return } + if await self.isWebChatBooted() { return } + } + await MainActor.run { + self.showError("web chat did not finish booting. Check that the gateway is running and try reopening.") + } + } + } + + 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 + cont.resume(returning: result as? Bool ?? false) + } + } + } + private func verifyReachable(endpoint: URL) async throws { var request = URLRequest(url: endpoint, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 3) request.httpMethod = "HEAD" @@ -238,11 +283,12 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N var frame = panel.frame frame.origin.x = round(anchor.midX - frame.width / 2) - frame.origin.y = anchor.minY - frame.height - 6 + frame.origin.y = anchor.minY - frame.height panel.setFrame(frame, display: false) } private func showError(_ text: String) { + self.bootWatchTask?.cancel() let html = """ Web chat failed to connect.

\( text) @@ -252,6 +298,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N func shutdown() { self.reachabilityTask?.cancel() + self.bootWatchTask?.cancel() self.stopTunnel(allowRestart: false) }