Mermaid Diagrams

When an agent includes a fenced code block with the language mermaid in its response, the session view renders it as an interactive diagram instead of plain code. The diagram supports pan and zoom, a reset-view control, a source/diagram toggle, fullscreen mode, and a per-node context menu that feeds the prompt input; its colours track the app's light/dark theme automatically.

Multiple diagrams in the same conversation render reliably on first paint, and rendering waits for custom web fonts to finish loading so text measurements line up with the laid-out diagram. You should not need to toggle between source and diagram mode to "fix" the layout — if a diagram looks broken, the source is almost always genuinely invalid.

How It Works

The agent writes standard Mermaid source inside a fenced code block:

```mermaid
graph TD
  Client --> APIGateway
  APIGateway --> Database
  APIGateway --> Cache
```

The source is rendered client-side using the official Mermaid library. Mermaid produces an inline SVG that is injected into the session card; no server round-trip is involved.

Streaming

Because agent responses stream token by token, the markdown layer waits for the code block to close before mounting the diagram. Every render, it scans the raw markdown for a closing ``` fence after the source; until that fence arrives, a loading placeholder is shown in place of the diagram. No debounce or delay — the moment the closing fence is present, the diagram mounts and Mermaid is invoked once on the complete source.

This replaces an earlier 600ms debounce that could fail in either direction: partial sources that happened to stabilise for 600ms would render and throw a parse error, and slow-streaming closing fences could trigger a re-render of already-correct content. Closing-fence detection is deterministic — a diagram mounts exactly when its source is complete.

If the source changes again after rendering (the agent revises the diagram on a subsequent turn), the diagram re-renders with the new source once its closing fence arrives.

Error Handling

If the Mermaid source is invalid and the parser throws, the block displays an error banner above the raw source, titled "Mermaid diagram failed to render" and showing Mermaid's parser error message. A copy-to-clipboard button next to the title copies just the error message, which is handy for pasting back into the prompt to ask the agent to fix it. The source code itself is rendered below the banner so the problem can be inspected and copied separately. The block does not disappear when parsing fails — the user always has the original source to work with.

Theme Matching

Mermaid's default palettes clash with the app's light and dark themes, so the renderer uses Mermaid's base theme and supplies a set of themeVariables read at render time from the app's :root CSS custom properties:

Mermaid variableSource CSS variable
background--color-bg
primaryColor, mainBkg--mermaid-node-bg
primaryBorderColor, nodeBorder--mermaid-node-border
primaryTextColor, textColor, titleColor--mermaid-node-text
secondaryColor, clusterBkg--mermaid-cluster-bg
secondaryBorderColor, clusterBorder--mermaid-cluster-border
tertiaryColor, edgeLabelBackground, labelBoxBkgColor--mermaid-label-bg
lineColor, signalColor--mermaid-line
sequenceNumberColor--color-bg
noteBkgColor--mermaid-note-bg
noteBorderColor--mermaid-note-border
noteTextColor--mermaid-note-text
fontFamily--font-body

The darkMode flag is also set to match the resolved theme so any variable that Mermaid computes internally gets a mode-appropriate default. The note variables are mapped explicitly because Mermaid defaults them to #fff5ad (yellow), which is illegible on dark backgrounds — the app's --mermaid-note-* custom properties fall back to the cluster palette so notes blend with the rest of the diagram.

When the user toggles theme (via settings or OS preference), a MutationObserver watching data-theme on the document element fires, the themeVariables are re-read, and the diagram re-renders with the new palette — no page reload needed.

Contrast-Aware Label Colours

Mermaid sources often include style / classDef directives (or, in sequence diagrams, rect rgb(r,g,b) background regions and overridden note fills) that paint a shape a specific colour without specifying a matching text colour. Without intervention, light theme text on a light-green override is unreadable.

After each render, the renderer computes WCAG 2.x relative luminance from the actual shape fill and writes a contrasting colour onto the overlapping text: #e8e9f3 on dark fills, #1a1a2e on light fills. Three cases are handled:

  • Flowchart / class nodes — every g.node / g.classGroup reads its shape's computed fill and recolours the child foreignObject labels and <text> elements.
  • Sequence diagram notes — each rect.note recolours its sibling text.noteText, so agent-overridden note fills stay legible.
  • Sequence diagram background regions — each rect.rect (from rect rgb(r,g,b) directives) recolours message-like text (text.messageText, text.labelText, text.loopText, text.sequenceNumber) whose viewport centre falls within the rect, and also restrokes message lines (line.messageLine0/1, path.messageLine0/1) so the connectors contrast with the region fill. Arrowheads are shared <marker> elements tinted via CSS, so for each recoloured line the marker is cloned into a per-colour variant with inline fill / stroke on its drawn paths and the line's marker-end / marker-start is repointed at the clone. The same clone-and-retint applies to the autonumber badges — whose circle lives in a *-sequencenumber marker — and text.sequenceNumber is given a double-pass contrast (against the new circle fill, not the rect) so the number still reads on the badge. Larger regions are processed first so nested regions win.

The per-element colour is applied directly (not via CSS), so it survives theme changes and overrides whatever palette Mermaid picked. If the agent does explicitly set color: in its directive, that still wins because the computed style reflects the override.

This replaces an earlier "text halo" approach (stroke around SVG text, four-corner text-shadow on foreignObject labels) that was visibly doubling characters. The contrast-aware pass gives the same legibility guarantee without any visual noise.

Diagram Controls

Pan and Zoom

Inline, the diagram does not intercept scroll or drag by default — scrolling over the diagram scrolls the page normally. Hold Shift to engage pan and zoom:

  • Shift + scroll zooms in and out around the cursor.
  • Shift + drag pans the viewport.

Zoom is bounded between 0.25× and 5×. There is no positional clamping, so a diagram can be panned off-screen; the Reset view toolbar button restores the initial fit-to-container view. Double-click zoom is disabled; use Shift + scroll for precise control.

In fullscreen, Shift is not required — pan and zoom are always active.

The pan-and-zoom implementation uses the panzoom library. panzoom transforms its target with transform-origin: 0 0 and its cursor-anchored zoom math assumes the target's layout position matches the owner's top-left. Attaching it to the SVG directly would break for diagrams smaller than the container — flex-centering offsets the SVG by half the empty space, and zoom would visibly slide away from the cursor by that amount. The renderer instead wraps the SVG in a full-size .acp-mermaid-pan-target div (flex-centering happens inside it) and attaches panzoom to the wrapper, so the math is exact regardless of diagram size. The only residual offset is the 1px border of the outer container, which is imperceptible.

Toolbar

A toolbar appears in the top-right corner of the diagram when you hover over it. It contains up to three buttons, depending on mode:

  • Reset view (diagram mode only, inline and fullscreen) snaps the pan offset back to zero and the zoom level back to 1, restoring the initial fit-to-container view after you've panned or zoomed.
  • Source/Diagram toggle switches between the rendered diagram and the raw Mermaid source. Useful for copying the source, diffing against what the agent generated, or inspecting a diagram that has rendered but isn't laid out the way you expected.
  • Fullscreen / Exit fullscreen expands the diagram to fill the entire viewport, or returns to inline. In fullscreen, pan and zoom work without holding Shift. Press Escape or click the exit button to return to the inline view.

Toolbar buttons fade in only when you hover the diagram, so they don't visually compete with the rest of the session content.

Node Context Menu

Clicking any node in the rendered diagram opens a floating menu at the cursor. Every diagram gets this automatically — no authoring required. The menu always offers two default actions:

  • Insert label — inserts the node's visible label into the prompt input (e.g. API Gateway)
  • Insert ID — inserts the node's Mermaid identifier (e.g. APIGateway)

Clicking either option dispatches the text through the same acp:insert-prompt-text event bus that LikeC4 diagrams use, so the text is appended to the current prompt (separated by a blank line when the prompt already contains text). Press Escape, click outside the menu, or choose an action to close it.

The click handler only fires on a clean click — if the pointer moves more than 5 pixels between mousedown and click (a Shift-drag pan in inline mode, or a drag pan in fullscreen), the menu is not opened.

Custom Actions with click ... call nodeMenu(...)

Agents can layer per-node prompt actions on top of the defaults by emitting Mermaid's built-in click directive with a call to the globally registered nodeMenu function. Mermaid only honours click on flowcharts (graph / flowchart) and class diagrams (classDiagram) — other diagram types ignore the directive, so this feature is limited to those two. The argument is a single double-quoted string containing one or more actions; actions are separated by ||, and each action uses the Label | Prompt text format:

```mermaid
graph TD
  APIGateway[API Gateway]
  Database
  APIGateway --> Database
  click APIGateway call nodeMenu("Explain auth | Explain how the API Gateway service authenticates requests, including token validation and the middleware chain||Review errors | Describe the error handling strategy in the API Gateway")
```

Clicking the APIGateway node then shows, in order:

  1. Explain auth
  2. Review errors
  3. (divider)
  4. Insert label
  5. Insert ID APIGateway

Custom actions appear above the divider; the defaults appear below. If an action string does not contain |, the entire string is used as both the label and the inserted text. Nodes without a click directive keep the default Insert label / Insert ID menu — authoring is incremental, one node at a time.

Prompting Agents to Add Node Actions

Mermaid only supports the click directive on flowcharts (graph / flowchart) and class diagrams (classDiagram). Sequence, state, ER, Gantt, pie, journey, git, requirement, and C4 diagrams silently ignore it, so there is no point authoring nodeMenu actions for those types.

Drop this into a persona, system prompt, or ad-hoc instruction to get richer per-node menus:

When emitting Mermaid diagrams, add per-node context-menu actions with:

click <NodeId> call nodeMenu("Label | Prompt||Label 2 | Prompt 2||...")
  • Only supported on flowcharts (graph / flowchart) and class diagrams (classDiagram). Do not emit click directives on sequence, state, ER, Gantt, pie, journey, git, requirement, or C4 diagrams — Mermaid ignores them there.
  • One double-quoted string. Separate actions with ||, label from prompt with |.
  • Label: 2–4 words. Prompt: a self-contained sentence that names the node and its role.
  • 2–5 actions per significant node. Focus on integration points, failure modes, data flows, boundaries, scaling.
  • No double quotes or literal || inside prompts. Only on top-level nodes — not edges, subgraphs, or class members.
  • Skip glue nodes (decision diamonds, generic actors); they keep the default Insert label / Insert ID menu.

Worked Example

A realistic diagram with per-node actions looks like this:

```mermaid
graph TD
  Client[Web Client]
  APIGateway[API Gateway]
  AuthService[Auth Service]
  OrderService[Order Service]
  OrderDB[(Order Database)]
  PaymentQueue[[Payment Queue]]

  Client --> APIGateway
  APIGateway --> AuthService
  APIGateway --> OrderService
  OrderService --> OrderDB
  OrderService --> PaymentQueue

  click APIGateway call nodeMenu("Routing logic | Describe how the API Gateway routes requests to Auth and Order services||Rate limiting | Explain the API Gateway rate-limiting and backpressure strategy")
  click AuthService call nodeMenu("Token flow | Walk through token issuance and validation in the Auth Service||Failure modes | Describe how the Auth Service degrades when its backing store is unreachable")
  click OrderService call nodeMenu("Order lifecycle | Walk through an order's state transitions inside the Order Service||Transaction boundaries | Explain how the Order Service coordinates writes to Order Database and Payment Queue")
  click OrderDB call nodeMenu("Schema overview | Summarize the Order Database schema and key relationships||Backup and recovery | Explain the Order Database backup cadence and recovery procedure")
  click PaymentQueue call nodeMenu("Delivery guarantees | Describe the Payment Queue delivery semantics and ordering guarantees||Dead letter handling | Explain how the Payment Queue handles messages that repeatedly fail")
```

Clicking APIGateway shows two custom actions plus the defaults; clicking Client (no click directive) shows just the defaults. This incremental pattern is the intended style — add directives where a follow-up prompt is genuinely useful, and leave the rest alone.

Supported Diagram Types

Mermaid supports a wide range of diagram types, all of which render via the same pipeline:

  • Flowcharts (graph TD, graph LR)
  • Sequence diagrams (sequenceDiagram)
  • Class diagrams (classDiagram)
  • State diagrams (stateDiagram-v2)
  • Entity-relationship diagrams (erDiagram)
  • Gantt charts (gantt)
  • Pie charts (pie)
  • Git graphs (gitGraph)
  • User-journey diagrams (journey)
  • Requirement diagrams (requirementDiagram)
  • C4 diagrams (C4Context, C4Container, etc.) — though for architecture work you will usually get a richer experience from LikeC4 diagrams.

See the Mermaid documentation for the full syntax of each type.

Styling Considerations

Mermaid sources can override the default palette on a per-node basis using style or classDef directives, for example:

graph TD
  RootLayout --> ThemeProvider
  style RootLayout fill:#eef,stroke:#4f46e5,stroke-width:2px

These overrides are respected by the renderer — you will see the explicit colours on the styled nodes. If the directive sets only fill and stroke, the contrast-aware label pass computes a readable text colour from the fill's luminance, so labels remain legible without any authoring effort. An agent can still include an explicit color: in its directive (style X fill:#eef,color:#111) and that wins — the contrast pass only fills in where the agent left the colour unspecified.

Choosing Between Mermaid and LikeC4

Both diagram types render interactively in the session view, but they serve different purposes:

  • Use Mermaid for general-purpose diagrams — flowcharts, sequence diagrams, state diagrams, Gantt charts, pie charts, and so on. Mermaid covers a broad set of diagram types in a single simple DSL.
  • Use LikeC4 for architecture diagrams where you want interactive navigation between views, element-level context menus that can populate the prompt input, structured metadata with per-element actions, and C4-model semantics (specification, model, views).

If you're unsure: Mermaid is the lighter-weight choice and will feel familiar to anyone who's used GitHub's fenced mermaid blocks. LikeC4 is worth the richer DSL when the diagram is going to be a reference artefact for the agent itself.