CLAUDE.md · git:20260917.f61e7df · 2026-09-17 · sha256 efe377a2b72a2ace

CLAUDE.md git:20260917.f61e7dfA

Immutable. This exact content never changes and is served at /api/v1/blob/efe377a2b72a2ace.

# flowpane

A Claude Code plugin that replaces the default Workflow progress list with a
live drawing of the run: a phase-by-phase graph, a stack, or a timeline in the
pane beside the transcript, with each agent's state, timing, token spend, tool
calls and prompt.

It draws into a terminal grid. Everything it knows comes from files a running
workflow writes, and everything it says has to fit in cells.

## Commands

```bash
claude plugin test .          # the test suite — 275 tests across 21 files
bunx tsc --noEmit             # typecheck (hooks/ and types/ only; see tsconfig.json)
```

Use `claude plugin test .`, not `bun test`. The tests import
`claude-code/testing`, which the plugin test runner supplies and a bare `bun
test` does not; bare `bun test` fails to resolve it.

The developer tools under `dev/` all take a run and draw it without a session:

```bash
bun dev/preview.ts [--animate|--journal <dir>|--settings|--menu theme|--about|--plain]
bun dev/shot.ts [--journal <dir>] [--port 8731]   # the pane in a browser, live, at any size
bun dev/stress.ts                                  # wide fan-outs and a narrow body
bun dev/lines.ts                                   # every line of every run at every size
bun dev/audit.ts                                   # every run, layout and size: nothing throws, every cell legible
bun dev/checkpic.ts <dir>                          # the bands cover the canvas, every node has a Button
bun dev/contrast.ts                                # every theme's roles against its ground, its edges against its states
bun dev/edges.ts <dir>                             # the graph derived for a run, and how it was derived
bun dev/recover.ts <sessionId> [home]              # what a session's runs rebuild to
bun dev/dryrun.ts <workflow>                       # a workflow's control flow, stubbed, no agents spent
bun dev/checkmeta.ts                               # the version the pane shows is the one the manifest ships
sh  dev/reload.sh                                 # install this tree as flowpane-dev, beside the released build
```

`dev/reload.sh` installs this tree *beside* the released plugin rather than in
place of it: `~/claude-dev-marketplaces/flowpane-dev` holds a copy of the repo
with the three names the engine keys off moved aside — the manifest's `name`,
`COMMAND` and `PANE_ID`. Renamed, both can be enabled at once: `/flowpane` stays
the released build and `/flowpane-dev` draws this tree. Unrenamed they collide on
every one of them — one command, one pane id, one `tool.call` hook opening one
pane. They are patched in the copy, never in the repo, so what ships is unchanged.

The title the pane draws is left alone. A build is told apart by the version it
already prints, and a product name bent out of shape to serve a dev install is a
dev install leaking into the picture.

A *copy*, not a symlink. A symlinked plugin source installs files that look
right — the engine's cache comes out byte-identical to the tree — and then never
loads, with no error printed anywhere: the command comes back `Unknown command`
while `claude plugin details` still reports the plugin as installed. So the
reload mirrors the tree in with `rsync`, then drops the engine's cached copy,
since an install over a version already installed is a no-op. Restart the session
afterwards: hooks are read once, when it starts.

Both builds watch the same journals and both open a pane on a Workflow launch,
so with the two enabled a live run draws twice. `/flowpane` toggles the released
one shut.

## Layout of the repo

| Path | What it holds |
| --- | --- |
| `hooks/register.ts` | The plugin itself: every hook, the journal tail, the run state |
| `hooks/journal.ts` | Reading a run off disk — journal, run file, agent transcripts |
| `hooks/shape.ts` | The run's shape: phases, passes, what fed what |
| `hooks/tree.ts` | The canvas as elements: coloured runs per row, and a Button over every hotspot |
| `hooks/layout.ts` | Where every band, card and column goes, at a given size |
| `hooks/paint.ts` | Everything drawn: cards, wires, header, dialogs, settings |
| `hooks/canvas.ts` | The grid: cells, colour mixing, line drawing, joints |
| `hooks/theme.ts` | Twelve themes, each ten colours, and the thirteen roles derived |
| `hooks/press.ts` | Keys and clicks |
| `hooks/about.ts` | What the pane says about itself: the dialog and `/flowpane about` |
| `tests/` | What the pane draws, read back off the canvas |
| `dev/` | The tools above |
| `docs/` | The design record: every decision, and what it replaced |
| `README.md` | What the plugin is, installing it, and the way in to `docs/` |

## Constraints the platform imposes

These are not preferences. Breaking one produces a pane that looks right in a
test and wrong in a terminal.

- **A `Button` carries no colour and no background.** `ButtonProps` has `key`,
  `label`, `hotkey`, `action`, `plain` and `hover` — no colour among them. A
  state mark, a rule or a fill drawn inside a Button's cells comes out in the
  label's own plain text. So every coloured cell stays outside every hotspot: the mark sits beside the
  name with a cell of air between them, and a card is framed in code points
  rather than filled.
- **Every dialog is modal.** There is no layering below one. Anything that has
  to be readable while a dialog is open is drawn into the pane, not under it.
- **A `Raster` is blitted, an element tree is re-rendered.** `ui.blit` repaints
  a mounted Raster at 120 ms a frame; a redraw through `ui.render` costs a
  render and runs at 320. Animation belongs in the Raster.
- **The pane's size is given, not asked for.** Every drawing decision takes the
  width it is handed. Nothing may assume a minimum beyond what `layout.ts`
  already guards.

## Rules the drawing keeps

- **Colour says one of two things: what state a node is in, or that a line is a
  wire.** Structure is grey. The one border allowed to carry a state is a
  card's own frame, which encloses exactly one agent — see `frameOf`.
- **No line crosses a word.** `canvas.line()` refuses to overwrite a port, and
  `dev/lines.ts` checks the whole corpus: every line lands on a blank cell,
  every arrowhead has a stem, every arm reaches something.
- **A measurement keeps its unit where the unit fits.** Token counts run down a
  ladder — `∑ 15.1k tkns`, `∑ 15.1k`, `15.1k` — and a lane picks one spelling
  for all of its cards, so two cards are never compared in two units.
- **The header gives way in a fixed order.** Each group has a `drop` rank and
  its own `short` ladder; the highest rank gives way first.
- **A finished run stops moving.** A run that is not `running` is painted at
  `tick = -1`, which freezes every animation: no spinner, no sliding label, no
  travelling light.
- **The arc is for two wires, and nothing else.** `◠` says *there is a second
  line in this cell, go and find it*. A phase's border and a band's rule are
  boundaries rather than lines anything travels along, so a wire meeting one
  breaks the boundary and runs through whole. A card's exit point is marked
  whatever else wants the cell.
- **A phase is entered where it started and left where it finished.** Its
  agents group into waves by their clocks — the ones that overlapped, in the
  order the waves went — and the barrier above it lands on the first wave, the
  phase below is fed from the last. One wave is a fan and keeps every wire;
  waves of one are a chain. Feeding every agent both ways draws two gutters of
  wire that say the phase started all of them at once; see `wavesOf`.
- **The dashed stroke means *not this run's own work*.** The phases it skipped,
  the strip naming the ones still ahead, and a nested run's rule and gutter —
  three things, one register, in every layout.
- **Time is drawn to one scale, and everything on it reads that scale from one
  place.** The timeline's axis has a width of its own — as many cells as the
  shortest bar in the run needs, up to four panes' — and the pane is a window on
  it. Bars, ticks, grid columns and the now-marker all take their column from the
  single millisecond-to-cell function, which subtracts the sideways scroll, and
  all clip to the axis columns rather than to the pane.
- **A section is never narrower than a name, and a node never shorter than its
  frame.** The pane divides itself only among the phases — and, down the pane,
  the agents — it can give a column wide enough to name. Past that the columns
  take the width they need, the drawing runs past the pane's edge and the body
  scrolls to the rest. The height keeps the same bargain: a column with more
  nodes than the pane can stand as cards keeps the cards and scrolls, rather
  than flattening every node in the drawing to a row. See `docs/header.md`.

## Conventions

- **Comments say why, not what.** Most of what is hard here is a decision that
  replaced something worse, and the comment is where the replaced thing is
  recorded. `paint.ts` is long because of this and that is the intent.
- **Prose written to a file is normal English** — comments, `docs/`, commit
  messages. No abbreviations invented for the file, no notes to the reader
  about the session that produced it.
- **`docs/` is the design record, and it is kept current.** A change that
  alters what a reader sees changes the page under `docs/` that covers it, in
  the same pass — `docs/README.md` says which page that is. The root `README.md`
  is the introduction: what the plugin is, how to install it, and links in.
- **Tests read the canvas.** They paint a run and assert on the cells, so a
  test says what a person would see. Keep the fixture minimal and name the test
  as a sentence about the drawing.