From 264131eb9f0c1c2709c08462829f7cb470d386f6 Mon Sep 17 00:00:00 2001
From: Mariano <132747814+mbelinky@users.noreply.github.com>
Date: Wed, 18 Feb 2026 19:44:55 +0000
Subject: [PATCH] Canvas: improve A2UI asset resolution and empty state
(#20312)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: adce485695e5a39f913334c6a903c7038594d735
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
---
CHANGELOG.md | 1 +
.../OpenClawKit/Tools/CanvasA2UI/bootstrap.js | 1 -
src/canvas-host/a2ui.ts | 25 +++++++++++++++++--
3 files changed, 24 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b0f74ece8..348adb01b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
### Fixes
+- Canvas/A2UI: improve bundled-asset resolution and empty-state handling so UI fallbacks render reliably. (#20312) Thanks @mbelinky.
- UI/Sessions: accept the canonical main session-key alias in Chat UI flows so main-session routing stays consistent. (#20311) Thanks @mbelinky.
- iOS/Onboarding: prevent pairing-status flicker during auto-resume by keeping resumed state transitions stable. (#20310) Thanks @mbelinky.
- OpenClawKit/Protocol: preserve JSON boolean literals (`true`/`false`) when bridging through `AnyCodable` so Apple client RPC params no longer re-encode booleans as `1`/`0`. Thanks @mbelinky.
diff --git a/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js b/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js
index 563adcc3b1..a9cb659876 100644
--- a/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js
+++ b/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js
@@ -451,7 +451,6 @@ class OpenClawA2UIHost extends LitElement {
if (this.surfaces.length === 0) {
return html`
Canvas (A2UI)
-
Waiting for A2UI messages…
`;
}
diff --git a/src/canvas-host/a2ui.ts b/src/canvas-host/a2ui.ts
index 0f65ab67ed..bac09a4438 100644
--- a/src/canvas-host/a2ui.ts
+++ b/src/canvas-host/a2ui.ts
@@ -13,14 +13,29 @@ export const CANVAS_WS_PATH = "/__openclaw__/ws";
let cachedA2uiRootReal: string | null | undefined;
let resolvingA2uiRoot: Promise | null = null;
+let cachedA2uiResolvedAtMs = 0;
+const A2UI_ROOT_RETRY_NULL_AFTER_MS = 10_000;
async function resolveA2uiRoot(): Promise {
const here = path.dirname(fileURLToPath(import.meta.url));
+ const entryDir = process.argv[1] ? path.dirname(path.resolve(process.argv[1])) : null;
const candidates = [
- // Running from source (bun) or dist (tsc + copied assets).
+ // Running from source (bun) or dist/canvas-host chunk.
path.resolve(here, "a2ui"),
+ // Running from dist root chunk (common launchd path).
+ path.resolve(here, "canvas-host/a2ui"),
+ path.resolve(here, "../canvas-host/a2ui"),
+ // Entry path fallbacks (helps when cwd is not the repo root).
+ ...(entryDir
+ ? [
+ path.resolve(entryDir, "a2ui"),
+ path.resolve(entryDir, "canvas-host/a2ui"),
+ path.resolve(entryDir, "../canvas-host/a2ui"),
+ ]
+ : []),
// Running from dist without copied assets (fallback to source).
path.resolve(here, "../../src/canvas-host/a2ui"),
+ path.resolve(here, "../src/canvas-host/a2ui"),
// Running from repo root.
path.resolve(process.cwd(), "src/canvas-host/a2ui"),
path.resolve(process.cwd(), "dist/canvas-host/a2ui"),
@@ -44,13 +59,19 @@ async function resolveA2uiRoot(): Promise {
}
async function resolveA2uiRootReal(): Promise {
- if (cachedA2uiRootReal !== undefined) {
+ const nowMs = Date.now();
+ if (
+ cachedA2uiRootReal !== undefined &&
+ (cachedA2uiRootReal !== null || nowMs - cachedA2uiResolvedAtMs < A2UI_ROOT_RETRY_NULL_AFTER_MS)
+ ) {
return cachedA2uiRootReal;
}
if (!resolvingA2uiRoot) {
resolvingA2uiRoot = (async () => {
const root = await resolveA2uiRoot();
cachedA2uiRootReal = root ? await fs.realpath(root) : null;
+ cachedA2uiResolvedAtMs = Date.now();
+ resolvingA2uiRoot = null;
return cachedA2uiRootReal;
})();
}