Display name of the session's agent (legend, chips, popover, toast).
The user clicked Undo on the agent-action toast (the undo itself already applied).
OptionalBoardBoard title shown in the header.
AFTER event (forwarded from the board): an editor commit applied.
Cancelable BEFORE event (forwarded from the board): an in-board editor commit is
about to write to the state engine. Set Cancel = true synchronously to discard it.
Opt back in to DOCUMENT-WIDE keyboard shortcuts.
The host binds eleven bare single-character tool shortcuts (v h p r s t m w i c e). While
those listened on document unconditionally they fired wherever the user was on the page,
which fails WCAG 2.1.4 (Character Key Shortcuts): a speech-input user saying an ordinary
word, or anyone typing in a non-input control elsewhere, silently switched the board tool.
They are now scoped to focus being inside this host, which satisfies the criterion's
"active only on focus" branch.
Scoping covers the host's ENTIRE keydown handler, not only the letters: undo/redo (Cmd/Ctrl+Z, +Y), Escape and Delete/Backspace are focus-gated as well.
Set true only for a surface where the whiteboard is the entire page AND the 2.1.4
exposure has been accepted — it is an accessibility regression, not a convenience flag.
Focus-board toggle — the session shell collapses/restores the call rail.
ReadonlyHostThe board is made focusable so its shortcuts have a focus scope to be bound to. -1, not
0: the canvas has no keyboard interaction model yet (see the whiteboard keyboard-
operability work), so adding a real Tab stop would put a stop in the page that a keyboard
user can reach and do nothing with. Click focuses it; that is the whole contract today.
AFTER mirror of WhiteboardState.ItemAdded$.
Cancelable BEFORE mirror of WhiteboardState.ItemAdding$.
AFTER mirror of WhiteboardState.ItemRemoved$.
Cancelable BEFORE mirror of WhiteboardState.ItemRemoving$.
AFTER mirror of WhiteboardState.ItemUpdated$.
Cancelable BEFORE mirror of WhiteboardState.ItemUpdating$ (update / move / reorder).
Render the board read-only: nothing on it can be created, edited, moved or deleted.
This is the OTHER axis from ToolRoster, and the two do not substitute for each
other. The roster answers "which tools does this surface offer" — it narrows a palette the
user still authors with. ReadOnly answers "may anything change at all" — it is about
mutation, not about choice, so an empty roster is not a read-only board and a full roster
on a read-only board still mutates nothing.
The board component already guards every mutation entry point and hides the page strip's
add / rename / delete affordances; the host adds the chrome that only it owns — the floating
toolbar is not rendered (matching WhiteboardSnapshotComponent, which draws a read-only
board with no toolbar), the agent toast's Undo is hidden because Undo applies a mutation,
and the keyboard handler returns before any key can act. Pan and zoom stay live: reading a
board you cannot change still means moving around it.
Persistence chip text (e.g. "Saved to session · v14").
Emitted when the user picks "Save to artifacts" — the channel plugin persists via its host context.
Debounced (750 ms), coalesced scene-delta JSON — the live perception feed.
Shared board state engine, owned by the integration layer.
AMBIENT widget telemetry (forwarded from the board): the injected recorder observed
passive form activity (clicks / changes / typing / focus) inside a sandboxed HTML
widget, validated + pre-summarized. Integration layers forward it to their agent
runtime as throttled, low-priority background context — e.g. MJ's channel plugin sends
an "ambient activity" [whiteboard] note at most once per widget per few seconds.
AFTER event: a sandboxed HTML widget submitted user input via MJWhiteboard.submit
(validated + size-capped by the board, not canceled). Integration layers forward it
to their agent runtime — e.g. MJ's channel plugin sends a [whiteboard] context note.
Cancelable BEFORE event (forwarded from the board): a sandboxed HTML widget submitted
input via MJWhiteboard.submit and it passed validation. Set Cancel = true
synchronously to drop it before WidgetSubmitted fires.
Whether the board has more than one page (gates the "all pages" export entries).
"2 s ago" style age of the last emitted delta (the ticker keeps it fresh).
The active tool, and the ONE place the roster invariant is enforced on a write.
"The active tool is always one the roster allows" is a property of every write, not of the
handful of call sites that exist today (the roster setter, Escape, the key map, and the two
template Tool = $event bindings). Guarding each of those would leave the next writer —
a new shortcut, a gesture, a host API — to remember the rule. Guarding here closes them all
by construction, and makes assignment order irrelevant: setting ToolRoster and Tool in
either order still lands on an allowed tool.
A DISALLOWED request is ignored rather than clamped: pressing Escape under ['pen','eraser']
should leave you on pen, not bounce you to the roster's fallback. The clamp is only for the
case where the tool you are already holding stopped being allowed.
ONE EXCEPTION to "always one the roster allows": a roster that allows nothing at all — empty,
or naming no real tool. The board must hold some tool, so ClampToolToRoster floors it
at select, which is then held even though the roster does not list it. That is deliberate
and is the safe answer precisely because select can create nothing; the alternative —
keeping the tool the roster just revoked — left a creating tool live with no toolbar to see
it and no key to change it. See ClampToolToRoster for why the floor is select
rather than the roster's first entry.
Which tools this surface offers. null (default) is all eleven: today's rendering.
The roster governs which tools are AVAILABLE, closing every door to a tool it leaves out:
the toolbar button, the single-letter shortcut, and the canvas "add … here" action. It is
deliberately not a content policy — it does not restrict what already exists on the board,
what the agent places, or authoring on existing items (Duplicate, z-order, Delete, paste).
The one exception is Restyle…, which is hidden when the roster omits text, because it
opens the TEXT tool's style flyout and would otherwise be a menu entry that does nothing.
An empty roster (or one naming no real tool) narrows the palette to nothing and clamps the
active tool to select. It does NOT make the board read-only — that is ReadOnly.
A setter rather than ngOnChanges (packages/Angular/CLAUDE.md): a realtime channel's
BindSurface assigns this property directly on a dynamically-created component, and
Angular fires no ngOnChanges for a plain assignment, so a clamp implemented there would
silently never run on the path real consumers use.
Execute one agent whiteboard tool (author 'agent', one undo batch), with the UI
garnish: presence cursor glides to the mutation point, the new item pops in violet,
and the action toast (with Undo) appears. Returns the result JSON string that the
integration layer feeds back to the realtime model as the tool_response.
Download ONE self-contained HTML document containing EVERY page (titled sections).
Download the ACTIVE page as a standalone SVG document.
Download EVERY page as its own standalone SVG document (SVG is single-image, so "all pages" is one file per page, downloaded sequentially with the page name slug-suffixed into each filename).
Print the board: open a blank window, write the self-contained export document and invoke the print dialog (same precedent as the artifact viewer's print path). When the popup is blocked this is a logged no-op — nothing breaks, the user can retry.
Request a first-class artifact snapshot of the board (handled by the channel plugin).
A callback method that performs custom clean-up, invoked immediately before a directive, pipe, or service instance is destroyed.
A callback method that is invoked immediately after the default change detector has checked the directive's data-bound properties for the first time, and before any of the view or content children have been checked. It is invoked only once when the directive is instantiated.
Clicking the board takes focus, so the shortcuts below apply. The browser would usually do
this on its own by focusing the nearest focusable ancestor, but the board's pointer
pipeline is elaborate enough (drag, marquee, handles, transient shapes) that relying on
that default is fragile — one preventDefault() added later would silently kill every
shortcut. This makes it explicit.
LIVE WHITEBOARD host (
mj-realtime-whiteboard-host): board header (title / saved chip / ownership legend / "What [Agent] sees" popover / Focus toggle), the board surface with its floating toolbar + zoom cluster, the agent action toast with Undo, and the status footer — everything EXCEPT the overlay/tab chrome, which the session shell owns.Integration contract:
Stateis created by the integration layer (channel state of record) and passed in;SceneDeltaemits the debounced (750 ms), coalesced scene-delta JSON on user changes — the perception feed the integration pipes into the live agent context;ApplyAgentTool(toolName, argsJson)executes one agent channel tool (Whiteboard_AddNote… seeWHITEBOARD_TOOL_DEFINITIONS) with the violet pop-in, toast and presence-cursor garnish, returning the result JSON for the tool round-trip;AgentUndofires when the user clicks Undo on the agent-action toast;FocusModeChangeasks the shell to collapse/restore the call rail ("Focus board").Extensibility (before/after events): the host mirrors the engine's cancelable BEFORE / AFTER mutation pairs as outputs (
ItemAdding/ItemAdded,ItemUpdating/ItemUpdated,ItemRemoving/ItemRemoved) and forwards the board's editor + widget pairs (ContentApplying/ContentApplied,WidgetSubmitting/WidgetSubmitted). All BEFORE events honorCancel = trueset synchronously by a handler.