Skip to content

Docs: build-time mermaid + tmux-layout diagram rendering#1071

Merged
tony merged 20 commits into
masterfrom
doc-improvements
Jun 30, 2026
Merged

Docs: build-time mermaid + tmux-layout diagram rendering#1071
tony merged 20 commits into
masterfrom
doc-improvements

Conversation

@tony

@tony tony commented Jun 29, 2026

Copy link
Copy Markdown
Member

Summary

  • Add a build-time mermaid pipeline: a mermaid directive renders fenced diagrams to inline SVG during the Sphinx build (via mmdc), so diagrams paint with the page — no client-side mermaid runtime, no layout shift, no staggered pop-in, and they survive gp-sphinx's SPA navigation as live DOM.
  • Add a tmux-layout directive that draws real tmux pane layouts as inline SVG in pure Python — the aafig successor, tmux-aware. Authors declare a screen size and a named layout; panes tile the screen with single dividers, top-left shell commands, pygments-highlighted in the site's code font and colours.
  • Theme-match both: diagrams adapt to light/dark from furo's own tokens and .highlight rules, so they look native in either theme.
  • Add docs/AGENTS.md (a docs-prose voice guide, symlinked from docs/CLAUDE.md) and rewrite the workspace-builders intro to lead with the concept rather than the config keys.
  • Provision the docs CI to render diagrams (mmdc + headless Chrome); without it the build still succeeds but degrades a diagram to its source text with a single warning.

Changes by area

Diagram extensions

  • docs/_ext/mermaid_inline.py: mermaid directive → HTML write-phase visitor that shells to mmdc, content-hash-caches per theme, normalizes the SVG (unique id, explicit width/height from the viewBox, stripped max-width), and inlines light+dark variants toggled by CSS. Falls back to a <pre> with a one-time warning when the renderer is absent.
  • docs/_ext/tmux_layout.py: tmux-layout directive. Computes filling geometry for tmux's named layouts (even-vertical/horizontal, main-vertical/horizontal, tiled) and emits one inline SVG — panes tile with single dividers, text top-left, pygments shell highlighting reusing furo's .highlight rules via fill: currentColor. Pure Python, no headless browser.
  • docs/_static/css/gp-diagram.css, gp-tmux-layout.css: theme-adaptive styling (light/dark), code-block-matched fonts and backgrounds.

Docs content & voice

  • docs/configuration/workspace-builders.md: concept-first intro, a tmuxp load → Workspace Builder → Attach tmux session flow diagram, libtmux linked.
  • docs/configuration/examples.md: the pane-layout examples now use tmux-layout (faithful tmux geometry) instead of ASCII art.
  • docs/AGENTS.md + docs/CLAUDE.md (symlink): the docs prose voice guide.

Toolchain & CI

  • .github/workflows/docs.yml: Node/pnpm setup, a Puppeteer-browser cache, and a version-matched Chrome install before the docs build.
  • docs/package.json + pnpm lock/workspace: pin @mermaid-js/mermaid-cli. docs/node_modules and the render cache are gitignored.

Design decisions

  • Build-time SVG, not client-side mermaid: client mermaid.run() renders after paint (layout shift + flash + per-diagram stagger) and, with gp-sphinx's synchronous View-Transition swap, the diagram would flash in after the crossfade. Build-time inline SVG paints with the page and rides SPA swaps untouched. The cost — owned explicitly — is a headless-Chrome build dependency (mmdc); there is no pure-Node path, since mermaid needs real browser text metrics.
  • tmux-layout is a Python directive, not a mermaid extension: tmux layouts are pure geometry, where mermaid's browser-based layout engine is all cost and no benefit. A Python renderer needs no browser, and reusing furo's .highlight CSS makes the panes pixel-identical to the site's code blocks in both themes — which baked-in mermaid colours can't do.
  • Cache keyed on the full render config: the on-disk render cache digest includes the theme variables and themeCSS, so a styling change busts the cache instead of serving a stale SVG.

Test plan

  • uv run ruff check . --fix && uv run ruff format . — clean
  • uv run mypy . — clean
  • uv run py.test --reruns 0 — green, including new tests/test_docs_mermaid.py and tests/test_docs_tmux_layout.py (NamedTuple-parametrized: SVG normalization, render cache idempotency, missing-renderer fallback, tmux geometry fills the screen, shell highlighting, the setup() contracts)
  • just build-docs — succeeds with no new warnings against the existing baseline; diagrams render as inline themed SVG on the workspace-builders and examples pages

Setup required

The docs build environment must provide the diagram renderer, or mermaid diagrams degrade to text:

  1. pnpm -C docs install
  2. pnpm -C docs rebuild puppeteer (installs the version-matched headless Chrome)

The docs.yml workflow already does this for CI.

tony added 19 commits June 29, 2026 05:08
why: The intro framed the feature as a set of config "keys" to set,
which is the implementation surface, not what a workspace builder
means to the reader. Users meet the YAML knobs last, not first.

what:
- Open by defining what a workspace builder is and that it works out
  of the box, so most readers can stop after the first paragraph
- Use progressive disclosure: default -> builder options
  (pane_readiness) -> swap builders -> write your own in Python
- Name the cost/benefit of the prompt wait explicitly
- Fold the narrative cross-reference onto "write your own" and drop
  the redundant standalone pointer below the table
why: The intro now points advanced readers at libtmux as the layer a
custom builder drives, but left it as bare text with no way to get
there.

what:
- Link the first libtmux mention to https://libtmux.git-pull.com/,
  matching the plain-link style used in topics/library-vs-cli.md
why: After the intro was rewritten to address the reader directly and
lead with the concept, the per-setting sections still read as terse
reference blurbs ("Selects the builder", "A catalog of..."), and one
still framed pane_readiness as a "key".

what:
- Rewrite the workspace_builder, workspace_builder_paths, and
  workspace_builder_options leads in second person, present tense
- Drop the remaining "key" framing in favor of naming the setting
- Leave the resolver order list, value table, alias list, and error
  text untouched — reference precision stays as-is
why: The docs/ prose voice (concept before configuration, reader
addressed directly, advanced parts marked opt-in) lived only in one
page's git history.

what:
- Add docs/AGENTS.md describing the docs/ prose voice and audience,
  scoped as a complement to the root AGENTS.md
- Exclude AGENTS.md from the Sphinx build so it is not an orphan doc
why: Claude Code reads CLAUDE.md; the symlink lets it pick up the same
docs/ voice guidance without a second copy to keep in sync.

what:
- Symlink docs/CLAUDE.md -> AGENTS.md, mirroring the repository root
- Exclude CLAUDE.md from the Sphinx build alongside AGENTS.md
why: Diagrams must paint with the page — instant, no layout shift, no
staggered pop-in — and must survive gp-sphinx SPA swaps. Client-side
mermaid.run() loses on every count: async render means CLS and a flash
of source, and spa-nav.js takes its View-Transition snapshot before the
SVG exists. Rendering to inline SVG at build time satisfies all of it
with zero per-page JavaScript, and the swapped .article-container then
carries finished SVG through SPA navigation with no re-render.

what:
- Add docs/_ext/mermaid_inline.py: a `mermaid` directive that defers to
  an HTML write-phase visitor, which shells to mmdc, content-hash-caches
  per theme, normalizes the SVG (unique id replacing mermaid's fixed
  "my-svg", explicit width/height from the viewBox, stripped max-width),
  and inlines light+dark variants; degrades to a text fallback with a
  one-time warning when the renderer is absent
- Add docs/_static/css/gp-diagram.css: dual-SVG light/dark toggle on
  body[data-theme], intrinsic sizing, wide-diagram scroll wrapper
- Wire docs/conf.py: register the extension, route plain mermaid fences
  via myst_fence_as_directive, exclude node_modules and the render cache
  from the Sphinx source tree
- Pin the renderer locally (docs/package.json + pnpm lock/workspace);
  gitignore docs/node_modules and docs/_mermaid_cache
- Add tests/test_docs_mermaid.py: NamedTuple-parametrized, test_id-first
  coverage of the normalizer, digest determinism, dual-theme emission,
  cache idempotency, missing-renderer fallback, and the setup() contract
why: The concept-first intro tells the reader a workspace builder
"turns a workspace configuration into a live tmux session". A small
pipeline diagram makes that sentence visual right where it is stated,
before the reader reaches the reference table.

what:
- Add a left-to-right mermaid flowchart after the opening paragraph:
  `tmuxp load <workspace-file>` -> Workspace Builder -> Attach tmux
  session, with edge labels echoing the intro vocabulary
why: The left-to-right flowchart rendered ~1141px wide and scaled down
hard in the content column, shrinking its labels. A top-down layout is
~276px wide, fits the column at full size, and reads as a vertical
load -> build -> attach pipeline.

what:
- Switch the mermaid flowchart from LR to TD
why: At mermaid's default wrappingWidth (200px) the "tmuxp load
<workspace-file>" node wrapped onto two lines. Raising it keeps node
labels on a single line so each box sizes to its own text.

what:
- Set flowchart.wrappingWidth=500 via in-source mermaid frontmatter on
  the workspace-builders fence
why: Diagrams render at build time via mmdc + a headless Chrome
(docs/_ext/mermaid_inline.py). The docs publish job had only uv and
just, so it would silently degrade every diagram to a text fallback
and ship docs without them.

what:
- Set up Node.js and pnpm in the docs build job
- Cache ~/.cache/puppeteer keyed on docs/pnpm-lock.yaml
- Install the docs JS deps and provision the version-matched Chrome
  via `pnpm -C docs rebuild puppeteer` before building
why: Diagrams rendered with mermaid's stock default/dark presets, whose
purple boxes clashed with the site brand. Mapping mermaid's base-theme
variables to gp-furo's tokens makes diagrams match the docs in both
light and dark mode, with no extra page weight.

what:
- Add light/dark _PALETTES from gp-furo-tokens: brand-blue borders,
  near-background fills, foreground text, and the furo system font stack
- Render via `mmdc -c` (theme=base + themeVariables) instead of the
  -t default/dark presets; bump the cache version to re-render
- Test that each palette defines the required flowchart colour variables
why: _discover_chrome matched only puppeteer's Linux cache layout
(chrome-linux64), so on macOS or Windows the build could not find an
installed Chrome and would degrade diagrams to a text fallback.

what:
- Add _chrome_glob(platform): the puppeteer-cache glob per OS
  (linux, macOS .app bundle, win64), doctested
- Resolve via _chrome_glob(sys.platform) in _discover_chrome
why: The diagram read uniformly: the literal command
`tmuxp load <workspace-file>` looked like the prose concept nodes, and
the edge-label background boxes hugged their text with no breathing room.

what:
- Inject theme-agnostic themeCSS into every rendered diagram: nodes
  tagged `:::cmd` render in the furo monospace stack so commands read as
  code (over the existing code-like fill), and edge labels gain padding;
  white-space:normal keeps a padded label wrapping instead of
  overflowing the box mermaid measured for it
- Tag the `tmuxp load` node with `:::cmd` on the workspace-builders page
- Bump the render cache version to re-render
why: On the rendered page (not in standalone mmdc output) the :::cmd
label fell left because the build's headless Chrome lacks the exact
monospace face, so the measured box is wider than the on-page text; and
each edge label showed two stacked boxes once padded, because mermaid
backs every edge label with three nested coloured elements.

what:
- Replace the themeCSS constant with _theme_css(theme): center node
  labels, and collapse mermaid's .labelBkg / .edgeLabel / rect
  backgrounds into a single padded chip on .edgeLabel p in the theme's
  label colour
- Bump the render cache version to re-render
why: The :::cmd label rendered left-of-centre on browsers whose
monospace face is narrower than the build's headless Chrome (e.g. macOS
SF Mono): the shrink-to-fit table-cell sits at the left of the wider box
mermaid measured. CSS that resizes the label to compensate (width/flex)
corrupts mermaid's build-time measurement pass and ballooned the
diagram to ~15000px tall. Separately, the render cache keyed only on
source+theme, so a themeCSS/palette change served a stale SVG from
docs/_mermaid_cache (which rm -rf docs/_build does not clear).

what:
- Center node labels with `display: table` + `margin: 0 auto`:
  shrink-to-fit positioning that re-centres without changing the
  measured size; verified centered even when the label renders far
  narrower than its box
- Fold the full mermaid config (themeVariables + themeCSS) into the
  cache digest via a new `extra` arg so any styling change busts the
  cache; extract _mermaid_config(); _render now takes the config JSON
- Update the cache-idempotency test for the new _render signature/theme
why: The example pane layouts used aafig ASCII art, which renders
outside the build-time mermaid pipeline and ignores the site theme.
mermaid's block-beta diagrams map directly onto tmux pane grids and
inherit the furo palette, light/dark variants, and SPA handling.

what:
- Replace the four `.. aafig::` blocks (short-hand, 2/3/4 panes) with
  ```mermaid block-beta diagrams reproducing each layout
- aafig is still used by docs/about_tmux.md, so the extension stays
why: block-beta diagrams render with a negative viewBox origin (e.g.
`viewBox="-5 -97 148 194"`) and carry inner-element viewBoxes. The size
regex assumed `viewBox="0 0 ..."` and matched an inner `0 0 10 10`, so
the root SVG was sized 10x10 and the diagram rendered as a tiny icon.

what:
- Anchor the viewBox regex to the root `<svg>` tag and accept a negative
  min-x/min-y, taking width/height from the 3rd/4th numbers
- Add a normalizer test case + doctest for the block-diagram viewBox
why: block-beta panes rendered with gaps, so a tmux pane layout read as
separate boxes rather than a single tiled window.

what:
- Set block.padding=0 in the mermaid config so block panes share
  dividers and fill a contiguous rectangle, like a tmux window
- Other diagram types ignore the block key
why: The example pane layouts used aafig ASCII art, then block-beta —
neither shows real tmux geometry or matches the site's code styling. A
purpose-built renderer draws faithful tmux layouts that read like the
terminal, with no headless-browser dependency.

what:
- Add docs/_ext/tmux_layout.py: a `tmux-layout` directive taking a screen
  size and a named layout (even-vertical/horizontal, main-vertical/
  horizontal, tiled) plus per-pane shell commands. It computes filling
  pane geometry and emits one inline SVG: panes tile with single
  dividers, text top-left, pygments-highlighted in the site's code font
  and colours (light/dark) via furo's own .highlight rules and
  `fill: currentColor`. Command builtins stay default-coloured; a gp
  prompt leads each line. Pure Python, theme-adaptive, no JS, SPA-safe
- Add docs/_static/css/gp-tmux-layout.css: pane background = the pygments
  code background, code font-size, kerning/letter-spacing matching `pre`
- Convert the examples (short-hand, 2/3/4 panes) from block-beta to
  tmux-layout; register the extension in conf.py
- Add tests/test_docs_tmux_layout.py: NamedTuple-parametrized coverage of
  the geometry (fills the screen), highlighting, pane splitting, size
  parsing, SVG structure, and the setup() contract
@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.56%. Comparing base (812dcca) to head (88b115d).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1071   +/-   ##
=======================================
  Coverage   82.56%   82.56%           
=======================================
  Files          31       31           
  Lines        2770     2770           
  Branches      518      518           
=======================================
  Hits         2287     2287           
  Misses        346      346           
  Partials      137      137           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

why: Record the v1.74.0 docs update for the changelog.

what:
- Add a Documentation entry for the build-time, theme-aware diagrams on
  the workspace-builders and examples pages
@tony tony merged commit 9c6bd26 into master Jun 30, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant