diff --git a/docs/docs.json b/docs/docs.json
index 84755db44c41..12f325496fe0 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -1752,6 +1752,7 @@
"web/index",
"web/control-ui",
"web/dashboard",
+ "web/dashboard-architecture",
"web/webchat",
"web/tui",
"web/lobster"
diff --git a/docs/docs_map.md b/docs/docs_map.md
index cf2443aca186..86bf5d914105 100644
--- a/docs/docs_map.md
+++ b/docs/docs_map.md
@@ -10551,6 +10551,26 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
- H2: Debugging/testing: dev server + remote Gateway
- H2: Related
+## web/dashboard-architecture.md
+
+- Route: /web/dashboard-architecture
+- Headings:
+ - H2: Vision
+ - H2: Concepts
+ - H2: UX flows
+ - H2: Interaction tiers
+ - H2: Widget model and hosting
+ - H3: Widgets host content; MCP apps are one content kind
+ - H3: Transcript display: one widget card
+ - H3: Server-sourced widgets (pinned MCP apps)
+ - H2: Layout: fluid grid
+ - H2: Data model (per-agent DB)
+ - H2: Protocol surface
+ - H2: Agent tools
+ - H2: What this replaces
+ - H2: Non-goals (this program)
+ - H2: Implementation plan
+
## web/dashboard.md
- Route: /web/dashboard
diff --git a/docs/web/dashboard-architecture.md b/docs/web/dashboard-architecture.md
new file mode 100644
index 000000000000..5e7124a2115a
--- /dev/null
+++ b/docs/web/dashboard-architecture.md
@@ -0,0 +1,333 @@
+---
+summary: "Session dashboards: architecture and implementation plan (technical design, pre-GA)"
+read_when:
+ - Implementing or reviewing the session dashboard (boards) feature
+ - Changing widget hosting, the widget bridge, or board storage
+title: "Dashboard Architecture"
+---
+
+
+Technical design document for the session dashboard feature, written before and
+during implementation. It is the source of truth for the build-out. When the
+feature ships, `/web/dashboard` becomes the user-facing page and this page stays
+as the architecture reference.
+
+
+## Vision
+
+Working with an agent today is a text stream. The dashboard makes it a
+workbench: the agent renders live, interactive widgets; the user pins them onto
+a persistent surface; chat docks to the side (or hides) and the main content is
+the board. You go from "talking to the agent" to "operating a control panel the
+agent built for you" without ever leaving the session.
+
+Principles:
+
+- **A board is a face of a session, not a new object.** Every session (thread)
+ has two faces: the transcript and the board. A session with no pinned widgets
+ is plain chat. Pin one widget and the board exists. Boards inherit the
+ session's identity, agent ownership, naming, pinning, and lifecycle. There is
+ no `dashboard_create`, no board registry, no separate ACL model.
+- **Agent parity.** Everything the user can do on a board, the agent can do
+ with tools: add/update/remove widgets, arrange them, manage tabs, switch the
+ visible tab, dock or hide the chat.
+- **Native, not embedded.** The board is Lit components in the Control UI shell
+ (the same design system as the rest of the app). Only widget _content_ is
+ sandboxed in iframes. No URL bar, no browser chrome.
+- **Small agent surface.** Widgets are addressed by stable name and updated in
+ place. Layout is a fluid auto-compacting grid; the agent speaks sizes and
+ anchors, never pixels or coordinates.
+- **Capabilities over trust.** Widget code is arbitrary agent-authored HTML/JS
+ in a hard sandbox. Reach (gateway data, actions, network) exists only through
+ a declared, operator-granted capability manifest.
+
+## Concepts
+
+| Concept | Definition |
+| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Session (thread) | Existing gateway session, keyed by stable `sessionKey`. Owned by an agent. |
+| Board | The widget face of one session. Exists iff the session has widgets/tabs. Survives `/new`/`/reset` (attached to `sessionKey`, not the transcript). |
+| Tab | A presentation page of a board: which widgets, their arrangement, and the chat dock state (`left`/`right`/`bottom`/`hidden`). Boards start with one implicit tab. |
+| Widget | Named, sandboxed HTML/JS program owned by the session. Addressed as `sessionKey` + `name`. Updated in place by name. |
+| Capability manifest | Per-widget declaration of reach: `data` (read bindings), `actions` (allowlisted verbs), `prompt` (send to session), `net` (allowed origins). |
+| Pin (widget) | Moving a transcript widget onto the session's board (user affordance or agent tool arg). Unpin removes it from the board. |
+| Pin (session) | Existing sidebar pinning of sessions. A pinned session with a board opens on its board face. |
+
+## UX flows
+
+- **Graduation:** agent calls `show_widget` in any chat → widget renders inline
+ in the transcript exactly as today → hover shows **Pin to dashboard** → widget
+ appears on the session's board. The agent can pass `pin: true` to do the same.
+- **Board view:** a session with a board gets a face toggle (Chat / Dashboard).
+ Board view = tab strip (only when >1 tab) + fluid grid + docked chat pane.
+ Chat dock is resizable, movable (left/right/bottom), and collapsible exactly
+ like the sidebar. Per-tab dock state is remembered.
+- **Drag:** user drags widgets; grid auto-compacts (widgets float up, neighbors
+ reflow). Resize by handle snaps to size steps. No pixel placement — for
+ anyone.
+- **Reset warning:** `/new` / `/reset` on a board-bearing session asks for
+ confirmation in the web UI ("context resets, the dashboard stays") and keeps
+ the board.
+- **Sidebar:** pinned sessions render their board face when they have one.
+ The Home session's board is the default "agent dashboard".
+- **Interactions** (three tiers, see below): silent state events, visible
+ prompt sends, and automation triggers.
+
+## Interaction tiers
+
+1. **State events (default).** Widget UI interactions the model should know
+ about but not respond to. `bridge.emitState({...})` appends a structured
+ session notice (same mechanism as group-activity notices). No agent turn is
+ started; the model sees accumulated notices on its next run.
+2. **Prompts (explicit talk).** `bridge.sendPrompt(text)` — requires user
+ activation; sends a visible user message into the session (the docked chat
+ shows it). Rate-limited; each send is user-confirmed unless the widget holds
+ the `prompt` capability grant.
+3. **Automation.** `bridge.runAction(name, args)` — fires a manifest-declared
+ action. Initial verb set: `cron.trigger` (run an existing cron job now) and
+ `binding.refresh`. Cron jobs already run in visible, isolated run-sessions
+ and can use a cheaper model: that is the "small model powers the widget"
+ path. No hidden sessions anywhere.
+
+## Widget model and hosting
+
+Widget HTML/JS is authored by the agent (typically via `show_widget`), wrapped
+in the standard document shell (CSP meta, size reporter, bridge bootstrap) and
+rendered in `