refactor(canvas): host A2UI via gateway

This commit is contained in:
Peter Steinberger
2025-12-20 12:17:27 +00:00
parent 13ebbd1a2b
commit ed001a5f55
28 changed files with 385 additions and 354 deletions

View File

@@ -99,10 +99,12 @@ public struct BridgeHello: Codable, Sendable {
public struct BridgeHelloOk: Codable, Sendable {
public let type: String
public let serverName: String
public let canvasHostUrl: String?
public init(type: String = "hello-ok", serverName: String) {
public init(type: String = "hello-ok", serverName: String, canvasHostUrl: String? = nil) {
self.type = type
self.serverName = serverName
self.canvasHostUrl = canvasHostUrl
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,23 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Canvas</title>
<style>
:root { color-scheme: light dark; }
html, body { height: 100%; margin: 0; }
body {
font: 14px system-ui, -apple-system, BlinkMacSystemFont, "Roboto", sans-serif;
background: #0b1020;
color: #e5e7eb;
overflow: hidden;
}
clawdis-a2ui-host { display: block; height: 100%; }
</style>
</head>
<body>
<clawdis-a2ui-host></clawdis-a2ui-host>
<script src="a2ui.bundle.js"></script>
</body>
</html>

View File

@@ -85,16 +85,6 @@
pointer-events: none;
z-index: 3;
}
#clawdis-a2ui-wrap {
position: fixed;
inset: 0;
display: none;
z-index: 2;
}
#clawdis-a2ui-wrap clawdis-a2ui-host {
display: block;
height: 100%;
}
#clawdis-status .card {
text-align: center;
padding: 16px 18px;
@@ -126,9 +116,6 @@
<div class="subtitle" id="clawdis-status-subtitle">Waiting for agent</div>
</div>
</div>
<div id="clawdis-a2ui-wrap">
<clawdis-a2ui-host></clawdis-a2ui-host>
</div>
<script>
(() => {
const canvas = document.getElementById('clawdis-canvas');
@@ -170,92 +157,6 @@
};
})();
(() => {
const wrap = document.getElementById('clawdis-a2ui-wrap');
if (!wrap) return;
const candidates = [
// iOS (SwiftPM resources flattened)
"a2ui.bundle.js",
// Android (assets keep directory structure)
"../CanvasA2UI/a2ui.bundle.js",
"CanvasA2UI/a2ui.bundle.js",
];
const loadScript = (src) =>
new Promise((resolve, reject) => {
const el = document.createElement("script");
el.src = src;
el.async = true;
el.onload = () => resolve();
el.onerror = () => reject(new Error(`failed to load ${src}`));
document.head.appendChild(el);
});
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const installVisibilityHooks = () => {
const api = globalThis.clawdisA2UI;
if (!api || typeof api.applyMessages !== "function") return false;
if (globalThis.__clawdisA2UIVisibilityHooksInstalled) return true;
globalThis.__clawdisA2UIVisibilityHooksInstalled = true;
const show = () => { wrap.style.display = "block"; };
const hide = () => { wrap.style.display = "none"; };
const sync = () => {
try {
const surfaces =
typeof api.getSurfaces === "function" ? api.getSurfaces() : [];
if (Array.isArray(surfaces) && surfaces.length > 0) show();
else hide();
} catch {
hide();
}
};
const origApply = api.applyMessages.bind(api);
api.applyMessages = (messages) => {
const res = origApply(messages);
sync();
return res;
};
const origReset = api.reset.bind(api);
api.reset = () => {
const res = origReset();
hide();
return res;
};
hide();
return true;
};
(async () => {
if (globalThis.clawdisA2UI) {
installVisibilityHooks();
return;
}
let loaded = false;
for (const src of candidates) {
try {
await loadScript(src);
loaded = true;
break;
} catch {
// try next
}
}
if (!loaded) return;
// Wait for custom element upgrade + connectedCallback to publish globalThis.clawdisA2UI.
for (let i = 0; i < 60; i += 1) {
if (installVisibilityHooks()) return;
await sleep(50);
}
})().catch(() => {});
})();
</script>
</body>
</html>

View File

@@ -28,40 +28,4 @@ import Testing
#expect(msg.contains("instance=ipad16_6 ctx={\"city\":\"Vienna\"}"))
#expect(msg.hasSuffix(" default=update_canvas"))
}
@Test func a2uiBundleSupportsAndroidBridgeFallback() 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("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)}"))
}
@Test func a2uiBundleStylesModalBackdrop() 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("::backdrop"))
#expect(js.contains("backdrop-filter: blur(6px)"))
}
}

View File

@@ -8,14 +8,17 @@ import { themeContext } from "@clawdis/a2ui-theme-context";
const modalStyles = css`
dialog {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 24px;
border: none;
background: none;
display: flex;
align-items: center;
justify-content: center;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 32px);
background: rgba(5, 8, 16, 0.65);
backdrop-filter: blur(6px);
display: grid;
place-items: center;
}
dialog::backdrop {

View File

@@ -5,7 +5,14 @@ import { defineConfig } from "rolldown";
const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, "../../../../..");
const fromHere = (p) => path.resolve(here, p);
const outputFile = path.resolve(here, "../../Sources/ClawdisKit/Resources/CanvasA2UI/a2ui.bundle.js");
const outputFile = path.resolve(
here,
"../../../../..",
"src",
"canvas-host",
"a2ui",
"a2ui.bundle.js",
);
const a2uiLitDist = path.resolve(repoRoot, "vendor/a2ui/renderers/lit/dist/src");
const a2uiThemeContext = path.resolve(a2uiLitDist, "0.8/ui/context/theme.js");