fix(a2ui): stabilize canvas host

This commit is contained in:
Peter Steinberger
2025-12-20 10:58:13 +00:00
parent 28938ddb32
commit cd5809d11f
4 changed files with 180 additions and 148 deletions

View File

@@ -6,6 +6,7 @@ import Foundation
actor MacNodeRuntime {
private let cameraCapture = CameraCaptureService()
@MainActor private let screenRecorder = ScreenRecordService()
private static let a2uiShellPath = "/__clawdis__/a2ui/"
// swiftlint:disable:next function_body_length
func handleInvoke(_ req: BridgeInvokeRequest) async -> BridgeInvokeResponse {
@@ -200,15 +201,7 @@ actor MacNodeRuntime {
}
private func handleA2UIReset(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
_ = try await MainActor.run {
try CanvasManager.shared.show(sessionKey: "main", path: nil)
}
let ready = try await CanvasManager.shared.eval(sessionKey: "main", javaScript: """
(() => Boolean(globalThis.clawdisA2UI))
""")
if ready != "true", ready != "true\n" {
return Self.errorResponse(req, code: .unavailable, message: "A2UI not ready")
}
try await self.ensureA2UIHost()
let json = try await CanvasManager.shared.eval(sessionKey: "main", javaScript: """
(() => {
@@ -235,9 +228,7 @@ actor MacNodeRuntime {
}
}
_ = try await MainActor.run {
try CanvasManager.shared.show(sessionKey: "main", path: nil)
}
try await self.ensureA2UIHost()
let messagesJSON = try ClawdisCanvasA2UIJSONL.encodeMessagesJSONArray(messages)
let js = """
@@ -255,6 +246,35 @@ actor MacNodeRuntime {
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: resultJSON)
}
private func ensureA2UIHost() async throws {
if await self.isA2UIReady() { return }
_ = try await MainActor.run {
try CanvasManager.shared.show(sessionKey: "main", path: Self.a2uiShellPath)
}
if await self.isA2UIReady(poll: true) { return }
throw NSError(domain: "Canvas", code: 31, userInfo: [
NSLocalizedDescriptionKey: "A2UI not ready",
])
}
private func isA2UIReady(poll: Bool = false) async -> Bool {
let deadline = poll ? Date().addingTimeInterval(6.0) : Date()
while true {
do {
let ready = try await CanvasManager.shared.eval(sessionKey: "main", javaScript: """
(() => String(Boolean(globalThis.clawdisA2UI)))()
""")
let trimmed = ready.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed == "true" { return true }
} catch {
// Ignore transient eval failures while the page is loading.
}
guard poll, Date() < deadline else { return false }
try? await Task.sleep(nanoseconds: 120_000_000)
}
}
private func handleSystemRun(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
let params = try Self.decodeParams(ClawdisSystemRunParams.self, from: req.paramsJSON)
let command = params.command

File diff suppressed because one or more lines are too long

View File

@@ -40,4 +40,16 @@ import Testing
#expect(js.contains("clawdisCanvasA2UIAction"))
#expect(js.contains("globalThis.clawdisCanvasA2UIAction"))
}
@Test func a2uiBundleWrapsDynamicCssValues() throws {
guard let url = ClawdisKitResources.bundle.url(forResource: "a2ui.bundle", withExtension: "js")
else {
throw NSError(domain: "Tests", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Missing resource a2ui.bundle.js",
])
}
let js = try String(contentsOf: url, encoding: .utf8)
#expect(js.contains("blur(${r(statusBlur)})"))
#expect(js.contains("box-shadow: ${r(statusShadow)}"))
}
}

View File

@@ -1,4 +1,4 @@
import { html, css, LitElement } from "lit";
import { html, css, LitElement, unsafeCSS } from "lit";
import { repeat } from "lit/directives/repeat.js";
import { ContextProvider } from "@lit/context";
@@ -169,9 +169,9 @@ class ClawdisA2UIHost extends LitElement {
color: rgba(255, 255, 255, 0.92);
font: 13px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Roboto", sans-serif;
pointer-events: none;
backdrop-filter: blur(${statusBlur});
-webkit-backdrop-filter: blur(${statusBlur});
box-shadow: ${statusShadow};
backdrop-filter: blur(${unsafeCSS(statusBlur)});
-webkit-backdrop-filter: blur(${unsafeCSS(statusBlur)});
box-shadow: ${unsafeCSS(statusShadow)};
z-index: 5;
}
@@ -190,9 +190,9 @@ class ClawdisA2UIHost extends LitElement {
color: rgba(255, 255, 255, 0.92);
font: 13px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Roboto", sans-serif;
pointer-events: none;
backdrop-filter: blur(${statusBlur});
-webkit-backdrop-filter: blur(${statusBlur});
box-shadow: ${statusShadow};
backdrop-filter: blur(${unsafeCSS(statusBlur)});
-webkit-backdrop-filter: blur(${unsafeCSS(statusBlur)});
box-shadow: ${unsafeCSS(statusShadow)};
z-index: 5;
}