You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The dashboard your agent builds with you. A layout-as-data Control-UI surface where an AI agent (or a human) composes tabs and widgets through one validated control plane — and agent-authored widgets run live inside a provably-safe sandbox. This issue is the master hub for the whole initiative: the architecture, every PR, the merge order, and how to try and test it.
📦 One package, shipped as a reviewable stack. 15 PRs, all green except one inherited check (see Review & test below). Demo (agent scaffolds a custom revenue-chart widget → approval card → sandboxed mount → live update, then the human keeps working):
Architecture — one store, three faces, two widget tiers
Everything that mutates a dashboard funnels through one validated store, so the agent, the CLI, and human drag/drop are the same guarded control plane. The agent never touches ui/ source — it composes only through the control plane a human uses.
flowchart TB
subgraph Authors["Who composes a dashboard — same validated control plane"]
A1["🤖 AI agent · agent tools<br/>(dashboard_tabs_create, _widget_scaffold, …)"]
A2["👤 Human · CLI<br/>(openclaw dashboard …)"]
A3["👤 Human · Control UI<br/>(drag / drop / collapse)"]
end
A1 --> GW
A2 --> GW
A3 --> GW
GW["Gateway RPC<br/>dashboard.workspace.* · widget.* · data.read"]
GW --> STORE
STORE["🔒 DashboardStore<br/>async mutex · atomic file write · hand-written validators<br/>20-entry undo ring · 256 KB cap"]
STORE --> DOC[["workspace.json<br/>layout-as-data: tabs · widgets · prefs · registry"]]
STORE -- "plugin.dashboard.changed (broadcast)" --> UI["🖥️ Control UI · Workspaces tab"]
UI --> GRID["Tabs → widget grid"]
GRID --> BUILTIN["✅ Builtin widgets — trusted tier<br/>charts · preview · notes · approvals · status"]
GRID --> CUSTOM["🧩 Custom widgets — sandboxed tier<br/>iframe sandbox=allow-scripts · opaque origin<br/>CSP connect-src 'none' · approval-gated"]
CUSTOM -. "postMessage bridge (parent resolves bindings)" .-> UI
Loading
The two-tier capability ladder (the mental model):
Builtin widgets — the trusted "native" tier. First-party, full-capability: preview, charts, log-tail, approvals, status. Maintainers + community contribute these via a small widget SDK.
Custom widgets — the sandboxed tier. Agent/user-authored, safe by construction (iframe sandbox="allow-scripts", opaque origin, CSP connect-src 'none', approval-gated). Games, bespoke tools, one-off apps — built live by the agent.
Write-back — the multiplier. A jailed, approval-gated setState turns both tiers from views into stateful apps: games persist, editors save, a full-bleed tab becomes a real web app.
Custom-widget lifecycle — why agent-authored code is safe to run
sequenceDiagram
participant Agent as 🤖 Agent
participant Store as DashboardStore
participant Op as 👤 Operator
participant Route as Static route (auth:"plugin")
participant Frame as Sandboxed iframe
Agent->>Store: dashboard_widget_scaffold (manifest + index.html)
Store-->>Op: registry status:"pending" — placeholder card, NO iframe exists yet
Op->>Store: Approve (routes through plugin-approvals)
Store-->>Route: status:"approved" → assets now served (pending/rejected → 404 server-side)
Route-->>Frame: index.html + locked CSP (connect-src 'none')
Frame->>UI: request only the bindings its manifest declared
Note over Frame,UI: no origin · no network · no gateway line —<br/>the trusted parent resolves bindings on the widget's behalf
Loading
🔍 Review & test the whole package
① The one maintainer action that unblocks everything. Every PR shows a single red check — dependency-guard — and most CI is skipping ("Authorize workflow actor"). Both clear with one touch: approve the fork-PR CI workflow + authorize the dependency-graph head SHA. No new package enters the graph (only the new plugin's own lockfile importer entry). That also runs the full e2e/integration lane = the runtime "real behavior proof."
② Merge order — bottom-up. All PRs are a cumulative stack on the still-unmerged Wave-1 base, so every one (including the base) shows GitHub CONFLICTING against main — this is uniform and expected, not a per-PR defect. Merge the foundation first (#101094 → #101097 → #101098; rebase the base once on latest main to clear the conflict), then each feature PR's conflict clears as its parent lands.
③ Run the whole feature in one branch. The base PR #101098 sits at the top of the foundation stack, so checking it out gives you the complete substrate in one branch:
gh pr checkout 101098
pnpm install && pnpm build
# start the gateway with the dashboard plugin enabled → open Control UI → the "Workspaces" tab
The default Overview-as-data workspace renders on first load. Then use the per-PR test map below.
Share a ?ws=<slug> link; Export then Import a workspace
Deep link resolves + back/forward works; imported custom widgets land pending (never auto-approved)
Security-sensitive features each carry an in-PR adversarial review (sandbox breakout, pub/sub cross-tab isolation, binding stream-allowlist/no-eval, action-form no-injection, gallery no-SSRF, private-tab server-side visibility, import→approval gate).
✅ Why it's correct — the integration-seam story
Wave 1's UI and gateway were each unit-tested against the other's mock and never wired together, so three contract-drift bugs shipped green: file bindings threw, every human widget mutation (drag/collapse/hide/remove/pin) was dead against the real gateway, and the initial load rendered an empty dashboard. All three are fixed at the base with wire-contract regression tests + a new integration test (ui-gateway-integration.test.ts) that drives the real client → real gateway handlers → real store and fails on any regression. The dashboard now actually works against the real gateway — the mock-gateway demo had hidden that it didn't.
Roadmap & PR map
Wave 1 (foundation):#101094 (backend: workspace.json document + control plane) · #101097 (Workspaces UI + 9 builtins + Overview-as-data) · #101098 (sandboxed custom widgets + approval flow; also carries the 3 seam fixes + the integration test).
The agent never touches ui/ source — it composes dashboards only through the same validated gateway / CLI / agent-tool control plane a human uses. Every widget is either a first-party builtin or a sandboxed custom widget. Core stays a bundled plugin plus one Control-UI tab.
📦 One package, shipped as a reviewable stack. 15 PRs, all green except one inherited check (see Review & test below). Demo (agent scaffolds a custom revenue-chart widget → approval card → sandboxed mount → live update, then the human keeps working):
Architecture — one store, three faces, two widget tiers
Everything that mutates a dashboard funnels through one validated store, so the agent, the CLI, and human drag/drop are the same guarded control plane. The agent never touches
ui/source — it composes only through the control plane a human uses.flowchart TB subgraph Authors["Who composes a dashboard — same validated control plane"] A1["🤖 AI agent · agent tools<br/>(dashboard_tabs_create, _widget_scaffold, …)"] A2["👤 Human · CLI<br/>(openclaw dashboard …)"] A3["👤 Human · Control UI<br/>(drag / drop / collapse)"] end A1 --> GW A2 --> GW A3 --> GW GW["Gateway RPC<br/>dashboard.workspace.* · widget.* · data.read"] GW --> STORE STORE["🔒 DashboardStore<br/>async mutex · atomic file write · hand-written validators<br/>20-entry undo ring · 256 KB cap"] STORE --> DOC[["workspace.json<br/>layout-as-data: tabs · widgets · prefs · registry"]] STORE -- "plugin.dashboard.changed (broadcast)" --> UI["🖥️ Control UI · Workspaces tab"] UI --> GRID["Tabs → widget grid"] GRID --> BUILTIN["✅ Builtin widgets — trusted tier<br/>charts · preview · notes · approvals · status"] GRID --> CUSTOM["🧩 Custom widgets — sandboxed tier<br/>iframe sandbox=allow-scripts · opaque origin<br/>CSP connect-src 'none' · approval-gated"] CUSTOM -. "postMessage bridge (parent resolves bindings)" .-> UIThe two-tier capability ladder (the mental model):
iframe sandbox="allow-scripts", opaque origin, CSPconnect-src 'none', approval-gated). Games, bespoke tools, one-off apps — built live by the agent.setStateturns both tiers from views into stateful apps: games persist, editors save, a full-bleed tab becomes a real web app.Custom-widget lifecycle — why agent-authored code is safe to run
🔍 Review & test the whole package
① The one maintainer action that unblocks everything. Every PR shows a single red check —
dependency-guard— and most CI isskipping("Authorize workflow actor"). Both clear with one touch: approve the fork-PR CI workflow + authorize the dependency-graph head SHA. No new package enters the graph (only the new plugin's own lockfile importer entry). That also runs the full e2e/integration lane = the runtime "real behavior proof."② Merge order — bottom-up. All PRs are a cumulative stack on the still-unmerged Wave-1 base, so every one (including the base) shows GitHub
CONFLICTINGagainstmain— this is uniform and expected, not a per-PR defect. Merge the foundation first (#101094 → #101097 → #101098; rebase the base once on latestmainto clear the conflict), then each feature PR's conflict clears as its parent lands.③ Run the whole feature in one branch. The base PR #101098 sits at the top of the foundation stack, so checking it out gives you the complete substrate in one branch:
The default Overview-as-data workspace renders on first load. Then use the per-PR test map below.
④ Per-PR test map — how to exercise each feature.
openclaw dashboard tabs create --title Financials; ask an agent to scaffold a custom widget; Approve the pending cardnotesstarter); type + reloadsetState; a staleexpectedVersionwrite is rejectedbuiltin:previewwidget bound to a URL; reloadbuiltin:chartwidget (type: line/bar/area/sparkline/gauge) bound to a seriesdocs/plugins/dashboard-widget-authoring.mdto author your ownagent-status+approvalsbuiltinsnotesbuiltin; edit; reloadstream:/computed:builtin:action-form(template: "Deploy {env}"); submitephemeralanswer-widgets self-sweep; Pin makes permanent?ws=<slug>link; Export then Import a workspacepending(never auto-approved)Security-sensitive features each carry an in-PR adversarial review (sandbox breakout, pub/sub cross-tab isolation, binding stream-allowlist/no-eval, action-form no-injection, gallery no-SSRF, private-tab server-side visibility, import→approval gate).
✅ Why it's correct — the integration-seam story
Wave 1's UI and gateway were each unit-tested against the other's mock and never wired together, so three contract-drift bugs shipped green: file bindings threw, every human widget mutation (drag/collapse/hide/remove/pin) was dead against the real gateway, and the initial load rendered an empty dashboard. All three are fixed at the base with wire-contract regression tests + a new integration test (
ui-gateway-integration.test.ts) that drives the real client → real gateway handlers → real store and fails on any regression. The dashboard now actually works against the real gateway — the mock-gateway demo had hidden that it didn't.Roadmap & PR map
Wave 1 (foundation): #101094 (backend:
workspace.jsondocument + control plane) · #101097 (Workspaces UI + 9 builtins + Overview-as-data) · #101098 (sandboxed custom widgets + approval flow; also carries the 3 seam fixes + the integration test).Flagship — the chat↔dashboard loop
Tier 0 — multipliers
setStatebridge capability + persisted state) — PR Workspaces: persist widget state in SQLite #101329Tier 1 — high-value builtins ("native" tier)
stream:Workspaces: add stream and computed bindings #101878)Tier 2 — the apps layer ("every tab is a web app")
Tier 3 — control-hub / multi-user
Tier 4 — distribution
?ws=+ regression tests; path-segment form deferred to a core-router change — see PR)Tracked on the desktop/native track (not this web-ui series)
How this stays lean
The agent never touches
ui/source — it composes dashboards only through the same validated gateway / CLI / agent-tool control plane a human uses. Every widget is either a first-party builtin or a sandboxed custom widget. Core stays a bundled plugin plus one Control-UI tab.