Render Mermaid diagrams and worktree images in agent Markdown

This commit is contained in:
Ian Gulliver
2026-10-15 07:27:17 -07:00
parent 1f44bc6d91
commit 4ca6f88861
26 changed files with 5613 additions and 227 deletions
+5 -1
View File
@@ -10,7 +10,7 @@
- `internal/agents`: the agent session manager. It owns the running Claude processes and records their activity as events.
- `internal/claude`: the Claude Code stream-json client (see `docs/claude-protocol.md`).
- `internal/push`: web push notifications for new inbox items (see "Notifications").
- `web/`: the SPA, frameworkless TypeScript compiled file-by-file to ES modules (no bundler). `web/src/*.ts` is the source; `web/src/api.gen.ts` holds types generated from `api/openapi.yaml`, and `web/src/api.ts` is the typed client over them. `go generate ./web` runs `npm ci`, regenerates the types and compiles `web/dist/`, which is committed so `go install` needs no Node. The binary embeds `index.html`, `style.css`, `dist/` and `src/` (for source maps). `byre serve --dev` serves `./web` from disk instead (run from the repo root); pair it with `npm --prefix web exec tsc -- --watch`.
- `web/`: the SPA, frameworkless TypeScript compiled file-by-file to ES modules (no bundler). `web/src/*.ts` is the source; `web/src/api.gen.ts` holds types generated from `api/openapi.yaml`, and `web/src/api.ts` is the typed client over them. `go generate ./web` runs `npm ci`, regenerates the types and compiles `web/dist/`, which is committed so `go install` needs no Node. The binary embeds `index.html`, `style.css`, `dist/`, `src/` (for source maps), `fonts/` and `vendor/` (Mermaid; see "Inbox"). `byre serve --dev` serves `./web` from disk instead (run from the repo root); pair it with `npm --prefix web exec tsc -- --watch`.
## Data
@@ -142,6 +142,10 @@ There is no snoozing. The `snoozed_until` and `snooze_until` columns are still i
Every Markdown view (inbox items and the transcript) renders a line `<details><summary>Title</summary>` (or `<details>` with the `<summary>` on the next line), Markdown content, and a line `</details>` as a collapsed section (`web/src/markdown.ts`; sections nest) showing only the title; session guidance and the `ask_human` schema tell agents to put long supporting detail in them.
Markdown images (`![alt](src)`, or `![alt](<src>)`; the plain form also accepts spaces, since worktree paths under `Application Support` have one) render inline, linking to the full image. A `src` that is an absolute path, a path relative to the worktree or a `file://` URL is loaded from `GET /api/tasks/{taskId}/image?path=` for the item's or event's task; a `data:image/` URL of PNG, JPEG, GIF or WebP is used as is; any other scheme, `http(s)` included, shows as a link with the alt text and is never fetched. An image that fails to load is replaced by "image not available: <src>". An inbox `image` attachment renders the same way from its `image` (data URL) or `path`.
A code block fenced as `mermaid`, and an inbox `diagram` attachment, render as a Mermaid diagram (`web/src/mermaid.ts`). The library is vendored: `web/package.json` pins `mermaid` as a devDependency, and `go generate ./web` copies `node_modules/mermaid/dist/mermaid.min.js` and its LICENSE into `web/vendor/`, which is committed and embedded. To upgrade, change the pinned version, `npm --prefix web install`, and regenerate. Nothing loads it until the first diagram is drawn; that injects one `<script src="/vendor/mermaid.min.js">` and initializes Mermaid with `securityLevel: "strict"` and the `base` theme, coloured from the custom properties in `web/style.css` as they resolve at that moment (so a light/dark switch applies from the next page load). Each diagram shows its source until its SVG is ready; source Mermaid rejects stays as source with "diagram not rendered: <error>" under it. The endpoint (`internal/server/images.go`) serves PNG, JPEG, GIF, WebP and SVG by extension, resolves symlinks on both the worktree and the file and answers 403 for anything outside the worktree, 404 for a missing file, 400 for another extension or a non-file, and 409 once the task is closed (its worktree is gone). Responses carry `X-Content-Type-Options: nosniff` and a sandboxing `Content-Security-Policy`, so an SVG opened directly runs no script, and `Cache-Control: no-cache` with `Last-Modified`, so a changed file is refetched.
Answers reach the agent as one message, listing each answer under the question or error it responds to, once the session is idle and none of its blocking questions are still waiting for the human. An `other` answer to a question tells the agent that if it asks something back, it should answer with `ask_human` using the item's `thread_id`. Every item change is an `inbox.created` or `inbox.updated` event carrying the whole item, including who resolved it.
## Changes