mac: harden webchat panel
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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
|
||||
//#endregion
|
||||
|
||||
@@ -25,6 +25,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
|
||||
private let remotePort: Int
|
||||
private var reachabilityTask: Task<Void, Never>?
|
||||
private var tunnelRestartEnabled = false
|
||||
private var bootWatchTask: Task<Void, Never>?
|
||||
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 = """
|
||||
<html><body style='font-family:-apple-system;padding:24px;color:#888'>Connecting to web chat…</body></html>
|
||||
@@ -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 = """
|
||||
<html><body style='font-family:-apple-system;padding:24px;color:#c00'>Web chat failed to connect.<br><br>\(
|
||||
text)</body></html>
|
||||
@@ -252,6 +298,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
|
||||
|
||||
func shutdown() {
|
||||
self.reachabilityTask?.cancel()
|
||||
self.bootWatchTask?.cancel()
|
||||
self.stopTunnel(allowRestart: false)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user