# Second Brain: building the workflow visual

This companion to the visual cookbook explains how to turn a documented process
into an explorable diagram: numbered steps, branching routes, gold sequence arrows,
linked skill files, and a synchronized step panel. The examples are newly adapted
from the existing viewer's workflow layout, highlighting, rendering, and navigation
patterns. They are building blocks for a fresh browser implementation.

Read the visual cookbook first for `project`, `fitCamera`, `sizeCanvas`, and
`selectedRoute`. The JavaScript examples here assume those functions are available
in the same application scope. Nothing in the diagram performs the described
process: playback only changes the illustrated step.

## The visual composition

Use a dark navy map as the main surface. Lay the complete process out from left to
right on desktop; on narrow screens progress from top to bottom. Keep branch
alternatives visible but subdued. A warm gold line identifies process sequence,
while thin blue lines connect a step to the skill files it references.

The current step gets a pale mint ring and its number in the selected route.
Previously visited steps retain a quieter mint trail. Future steps on the chosen
route stay readable, and other branches recede. Use a diamond for a decision and
a circle for an action. Show branch labels in both the diagram and a normal select
control so color is never the only way to understand the path.

Keep a panel beside the map containing the workflow title, decision choices, a
numbered step list, the current explanation, and linked-file buttons. Put Previous,
Play/Pause, Next, Restart, Center workflow, and Center step controls below the map.
On phones, stack the sequence panel below it. Keep both in one fullscreen wrapper.

The core lesson: maintain three separate objects—complete graph, chosen route,
and playback position. Branch selection changes highlighting and the numbered
list; it does not regenerate the spatial layout.

## 1. Give steps their own identities

A process step is not a file. The same skill may participate in several steps, and
a human action may reference no skill. Store links as annotations on step records.
For example, `summarize` and `final-review` below both use the same skill but remain
two separate steps. This fictional fixture has a branch and a later merge.

```js
const exampleWorkflow = {
  id: 'review-notes', title: 'Review meeting notes', start: 'read',
  steps: [
    {id:'read', title:'Read the notes', kind:'human', next:'summarize', files:[]},
    {id:'summarize', title:'Summarize', kind:'agent', next:'ready',
      files:['work/summarize-notes/SKILL.md']},
    {id:'ready', title:'Are details complete?', kind:'decision', files:[],
      choices:[{label:'Yes',next:'final-review'},{label:'No',next:'clarify'}]},
    {id:'clarify', title:'Clarify missing details', kind:'human', next:'final-review', files:[]},
    {id:'final-review', title:'Review the summary', kind:'human', next:'done',
      files:['work/summarize-notes/SKILL.md']},
    {id:'done', title:'Ready to use', kind:'outcome', files:[]}
  ]
};
```

In real records, also keep a description and provenance. Use source-qualified file
identities. A process description is evidence of what was documented, not evidence
that an agent or a person executed it.

## 2. Lay out the full graph once

Calculate levels using topological order, placing each target after its longest
incoming path. This gives merged branches enough space even when their lengths
differ. Stable input order keeps sibling placement deterministic. The first version
supports acyclic workflows. This helper validates graph structure; validate field
types and text lengths when accepting a user's record as a separate input step.

Adapted from the level-based approach in `workflow-core.mjs`, with explicit checks
for missing targets, duplicate IDs, cycles, and unreachable steps:

```js
function layoutWorkflow(workflow, narrow = false) {
  const byId = new Map();
  for (const step of workflow.steps) {
    if (!step.id || byId.has(step.id)) throw new Error('Duplicate or empty step ID.');
    byId.set(step.id, step);
  }
  if (!byId.has(workflow.start)) throw new Error('Missing start step.');
  const edges = [];
  for (const step of workflow.steps) {
    if (step.kind === 'decision') {
      if (step.next != null || !Array.isArray(step.choices) || step.choices.length < 2)
        throw new Error('A decision needs at least two choices and no direct next.');
      step.choices.forEach((choice, choiceIndex) => edges.push({
        source:step.id, target:choice.next, label:choice.label, choiceIndex
      }));
    } else if (step.next != null) edges.push({source:step.id,target:step.next});
  }
  const outgoing = new Map(workflow.steps.map(s => [s.id, []]));
  const indegree = new Map(workflow.steps.map(s => [s.id, 0]));
  for (const edge of edges) {
    if (!byId.has(edge.target)) throw new Error('Missing target: ' + edge.target);
    outgoing.get(edge.source).push(edge);
    indegree.set(edge.target, indegree.get(edge.target) + 1);
  }
  const reachable = new Set(), stack = [workflow.start];
  while (stack.length) {
    const id = stack.pop();
    if (reachable.has(id)) continue;
    reachable.add(id);
    for (const edge of outgoing.get(id)) stack.push(edge.target);
  }
  if (reachable.size !== byId.size) throw new Error('Unreachable process steps.');
  const queue = workflow.steps.filter(s => indegree.get(s.id) === 0).map(s => s.id);
  const level = new Map([[workflow.start, 0]]), order = [];
  for (let i = 0; i < queue.length; i++) {
    const id = queue[i]; order.push(id);
    for (const edge of outgoing.get(id)) {
      level.set(edge.target, Math.max(level.get(edge.target) ?? 0, level.get(id) + 1));
      indegree.set(edge.target, indegree.get(edge.target) - 1);
      if (indegree.get(edge.target) === 0) queue.push(edge.target);
    }
  }
  if (order.length !== byId.size) throw new Error('Cycles are not supported.');
  const layers = new Map();
  for (const id of order) {
    const n = level.get(id);
    if (!layers.has(n)) layers.set(n, []);
    layers.get(n).push(id);
  }
  const positions = new Map();
  for (const [depth, ids] of layers) ids.forEach((id, lane) => {
    let cross = (lane - (ids.length - 1) / 2) * 180;
    const incoming = edges.filter(edge => edge.target === id);
    if (ids.length === 1 && incoming.length === 1 && incoming[0].choiceIndex != null) {
      const edge = incoming[0], count = byId.get(edge.source).choices.length;
      cross = (edge.choiceIndex - (count - 1) / 2) * 180;
    }
    positions.set(id, narrow
      ? {x:cross,y:depth * 170,z:0}
      : {x:depth * 220,y:cross,z:0});
  });
  return {byId, edges, positions};
}
```

Keep process steps near the front plane; place their linked files slightly behind
and below them, such as z = 40 and a positive y offset in a front view. The meaning of depth
is a visual grouping choice. Give each displayed file occurrence a unique key
combining step ID and file ID, while retaining its original file ID for inspection.
Two occurrences can then highlight separately without duplicating the underlying
file record. For many attached files, show a compact count and list in the panel
rather than a dense halo of overlapping satellites.

This simple layer layout is intended for small and medium authored processes.
It does not minimize edge crossings in complex graphs. For larger examples, refine
lane ordering and route long edges around intervening nodes before adding effects.
Never recompute positions merely because the user selected another branch.

## 3. Derive every visual state from one playback position

Use `selectedRoute` from the visual cookbook to obtain an ordered route. Then
derive current, visited, upcoming, and alternative steps from that route. Track the
selected branch index on decision edges as well as their endpoints: two choices
can lead to the same target and still have different labels.

Adapted from the original workflow highlighting, with separate edge states:

```js
function workflowVisualState(workflow, choices, index, graph) {
  const route = selectedRoute(workflow, choices);
  const position = Math.max(0, Math.min(route.length - 1, index));
  const current = route[position];
  const routeIndex = new Map(route.map((step, i) => [step.id, i]));
  const nodes = new Map(workflow.steps.map(step => {
    const i = routeIndex.get(step.id);
    return [step.id, {
      status:i == null ? 'alternative' : i < position ? 'visited' :
        i === position ? 'current' : 'upcoming',
      number:i == null ? null : i + 1
    }];
  }));
  const edges = graph.edges.map(edge => {
    const i = routeIndex.get(edge.source);
    const selectedChoice = Object.hasOwn(choices, edge.source) ? choices[edge.source] : 0;
    const chosen = i != null && route[i + 1]?.id === edge.target &&
      (edge.choiceIndex == null || edge.choiceIndex === selectedChoice);
    const status = !chosen ? 'alternative' : i + 1 === position ? 'current' :
      i + 1 < position ? 'visited' : 'upcoming';
    return {...edge, status};
  });
  return {route, position, current, nodes, edges};
}
```

The highlighted current edge is the incoming transition to the current step.
At step one there is no current edge. This avoids implying that playback already
performed the next step. Keep a legend explaining that these are diagram states.

On a branch change, pause, update the choice map, compute the route again, and
retain the current step ID if it still belongs to the route. Otherwise choose the
decision that changed or the first step. A missing reference should remain visible
as an unresolved annotation rather than preventing unrelated steps from displaying.

## 4. Draw the sequence arrows in screen space

Project world positions with the cookbook camera before drawing. This example
shortens each arrow so it meets the node boundaries, uses a gold dashed stroke,
and keeps a readable arrowhead. Optional motion is limited to the current edge
while playback is active. The motion is decorative, not a completion indicator.

Adapted from the workflow overlay in `render.js`:

```js
function drawSequenceArrow(ctx, a, b, status, time, playing, reducedMotion) {
  const dx = b.x - a.x, dy = b.y - a.y, length = Math.hypot(dx, dy);
  const from = a.r + 7, to = b.r + 10;
  if (length <= from + to + 12) return;
  const ux = dx / length, uy = dy / length;
  const start = {x:a.x + ux * from,y:a.y + uy * from};
  const end = {x:b.x - ux * to,y:b.y - uy * to};
  const alpha = {current:1,visited:0.65,upcoming:0.4,alternative:0.12}[status];
  ctx.save();
  ctx.globalAlpha = alpha;
  ctx.strokeStyle = status === 'current' ? '#ffe1a1' : '#caa66b';
  ctx.fillStyle = ctx.strokeStyle;
  ctx.lineWidth = status === 'current' ? 2 : 1;
  ctx.setLineDash([5,7]);
  ctx.beginPath(); ctx.moveTo(start.x,start.y); ctx.lineTo(end.x,end.y); ctx.stroke();
  ctx.setLineDash([]);
  ctx.beginPath(); ctx.moveTo(end.x,end.y);
  ctx.lineTo(end.x - ux * 8 - uy * 4,end.y - uy * 8 + ux * 4);
  ctx.lineTo(end.x - ux * 8 + uy * 4,end.y - uy * 8 - ux * 4);
  ctx.closePath(); ctx.fill();
  if (status === 'current' && playing && !reducedMotion) {
    const t = (time % 2200) / 2200;
    ctx.beginPath();
    ctx.arc(start.x + (end.x-start.x)*t,start.y + (end.y-start.y)*t,2.5,0,Math.PI*2);
    ctx.fill();
  }
  ctx.restore();
}
```

Draw branch labels near their edges using the cookbook's collision placement rules.
The edge records preserve labels, but this helper deliberately draws only geometry.
Keep the decision select and panel labels authoritative when graph labels crowd.
For curves, evaluate both the curve position and its tangent when placing an
arrowhead; using the straight endpoint direction produces incorrectly angled heads.

## 5. Render the current step as a clear focal point

Nodes use screen coordinates and an explicit radius. Draw edges first, then steps
in depth order, then labels. The current route number is part of the selection cue.
Place the full title below or beside the node with collision handling; it also
appears in the panel. A small luminous core works better than a large blurry glow.

```js
function drawProcessStep(ctx, point, step, appearance) {
  const colors = {current:'#c9fff0',visited:'#76bda8',upcoming:'#e2bc78',alternative:'#78909f'};
  const opacity = {current:1,visited:0.75,upcoming:0.6,alternative:0.25};
  const {x,y,r} = point;
  ctx.save();
  ctx.globalAlpha = opacity[appearance.status];
  ctx.fillStyle = colors[appearance.status];
  ctx.strokeStyle = ctx.fillStyle;
  if (appearance.status === 'current') {
    ctx.lineWidth = 2;
    ctx.beginPath(); ctx.arc(x,y,r+9,0,Math.PI*2); ctx.stroke();
  }
  ctx.beginPath();
  if (step.kind === 'decision') {
    ctx.moveTo(x,y-r);ctx.lineTo(x+r,y);ctx.lineTo(x,y+r);ctx.lineTo(x-r,y);ctx.closePath();
  } else ctx.arc(x,y,r,0,Math.PI*2);
  ctx.fill();
  if (appearance.number != null) {
    ctx.globalAlpha = 1; ctx.font = '600 14px system-ui';
    ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
    ctx.fillText(String(appearance.number),x,y-r-14);
  }
  ctx.restore();
}
```

Linked file occurrences use smaller blue/teal nodes with thin solid association
lines. Do not draw them as additional numbered steps. When a process step is
current, highlight its file occurrences and show the underlying file details in
the inspector. Use occurrence keys for drawing and original file IDs for lookup.

## 6. Assemble one frame from the shared state

Here is the glue between layout, playback, camera, and rendering. The complete graph
is laid out once; each frame updates only visual state and screen projection.
Use the cookbook's clear viewport and fit functions when entering a workflow or
when the user chooses Center workflow. Center step should frame the step and its
visible file occurrences within that same viewport.

```js
function drawWorkflowFrame(ctx, workflow, graph, choices, index, camera, view,
  time, playing = false, reducedMotion = false) {
  const visual = workflowVisualState(workflow, choices, index, graph);
  const screen = new Map();
  for (const step of workflow.steps) {
    const p = project(graph.positions.get(step.id), camera, view);
    if (p) screen.set(step.id, {...p,r:10 * p.perspective});
  }
  ctx.save();
  ctx.beginPath();ctx.rect(view.x,view.y,view.width,view.height);ctx.clip();
  for (const edge of visual.edges) {
    const a = screen.get(edge.source), b = screen.get(edge.target);
    if (a && b) drawSequenceArrow(ctx,a,b,edge.status,time,playing,reducedMotion);
  }
  for (const [id,p] of [...screen].sort((a,b) => b[1].depth-a[1].depth))
    drawProcessStep(ctx,p,graph.byId.get(id),visual.nodes.get(id));
  ctx.restore();
  return {visual,screen};
}
```

The caller clears the canvas once per frame. Add the file-occurrence layer and
collision-managed title labels around this core pass. Returning `screen` lets
pointer hit testing use the exact same projected positions. Returning `visual`
lets the panel use the same route, current step, and statuses as the canvas.
These omissions are intentional extension points, not completed behavior.

## 7. Keep the panel and map together

Use normal HTML controls, not canvas-drawn buttons. This structural example keeps
the heading, map, controls, and sequence together for responsive layout and fullscreen.

```html
<section class="workflow-view" aria-label="Documented workflow">
  <header class="workflow-heading"><h2>Review meeting notes</h2><p>Documented workflow</p></header>
  <div class="workflow-map"><canvas tabindex="0" aria-label="Workflow diagram; steps also listed alongside"></canvas></div>
  <nav class="workflow-controls" aria-label="Workflow playback">
    <button type="button">Previous</button><button type="button">Play</button>
    <button type="button">Next</button><button type="button">Center workflow</button>
  </nav>
  <aside class="workflow-sequence" aria-label="Workflow steps">
    <label>Are details complete? <select><option>Yes</option><option>No</option></select></label>
    <ol></ol><h3>Why this step?</h3><p class="step-description"></p>
  </aside>
</section>
```

```css
.workflow-view { display:grid; grid-template-columns:minmax(0,1fr) 20rem;
  grid-template-areas:"heading sequence" "map sequence" "controls sequence";
  grid-template-rows:auto minmax(24rem,1fr) auto; gap:1rem;
  background:#0b1626; color:#dbe8f3; padding:1rem; }
.workflow-heading { grid-area:heading; }
.workflow-map { grid-area:map; min-width:0; position:relative; }
.workflow-map canvas { display:block; width:100%; height:100%; }
.workflow-controls { grid-area:controls; display:flex; flex-wrap:wrap; gap:.5rem; }
.workflow-sequence { grid-area:sequence; overflow:auto; min-width:0; }
.workflow-view button,.workflow-view select { min-height:44px; font:inherit; }
.workflow-sequence button { width:100%; text-align:left; }
.workflow-sequence [aria-current="step"] { border:1px solid #9ae4ce; background:#193c3c; color:#e3fff5; }
.workflow-view :focus-visible { outline:2px solid #c9fff0; outline-offset:3px; }
.workflow-view:fullscreen { box-sizing:border-box; height:100dvh; }
@media(max-width:900px) { .workflow-view { grid-template-columns:minmax(0,1fr);
  grid-template-areas:"heading" "map" "controls" "sequence";
  grid-template-rows:auto minmax(22rem,1fr) auto auto; }
  .workflow-view:fullscreen { overflow:auto; } }
```

Since the grid reserves a separate map cell, the canvas viewport can normally use
that cell's interior. If your design overlays panels on top of the canvas, use the
clear-viewport measurement described in the visual cookbook instead. Do not both
reserve a grid column and subtract its width again.

Populate list items using `textContent` for user-supplied titles and descriptions.
Give the current step button `aria-current="step"`; remove it from other buttons.
Each list button seeks that step on the chosen route. Selecting a dim alternative
step on the graph can inspect it, but should not silently change the selected branch.

## Playback and navigation wiring

Maintain a single state object containing workflow ID, choices, current step ID,
playing, speed, followStep, and camera. Keep layout outside playback state.

1. On open: save the Atlas camera/filters/selection, load the workflow, set a front
   view, lay out all steps, measure the map, and fit. Begin paused with Follow step off.
2. On Next/Previous/list selection: pause and choose a valid route step. Update the
   canvas and panel from one `workflowVisualState` result.
3. On branch change: pause, change the choice, preserve a valid current step, and
   rebuild the list. Keep the full-graph positions unchanged.
4. For timed highlighting: use the remaining-time approach from the visual cookbook.
   Advance an index only once per interval; clear the old timer before seeking.
5. On manual camera movement: turn Follow step off. Opening Help should not do so.
6. On hidden tab: pause. Reduced motion suppresses camera transitions and moving
   particles; retain static status cues and manual exploration.
7. On close: pause, clear timers, and restore the exact saved Atlas state. Keep its
   camera history separate from the workflow camera history.

Use a common update path for the controls, canvas, and panel. Do not implement a
separate "current step" in each component. Announce meaningful manual step changes
in a polite status region; avoid flooding screen readers during decorative redraws.

## What made the original workflow view work well

- Gold sequence arrows remained distinguishable from file relationships.
- The step list supplied the explanation that the graph alone could not convey.
- Numbering belonged to the selected route; branch alternatives retained titles.
- Repeated skill use was represented by separate process steps.
- Center workflow fitted the visible route without hiding it behind the panel.
- Fullscreen retained the sequence panel, controls, and exit path.
- Manual camera gestures took precedence over automatic following.
- Pausing, branch changes, and closing all preserved predictable navigation state.

## Checks for the receiving agent

Test both branches, a branch merge, a repeated linked file, a missing linked file,
an invalid target, an unreachable step, and a cycle. At step one no incoming arrow
is current; after advancing exactly one chosen transition becomes current. Confirm
that alternative choices sharing a target are not both highlighted.

Check wide and narrow layouts, rotate then center, seek from the list, pause near a
step boundary, switch branches while playing, hide the tab, and return from fullscreen.
The numbered list and canvas should agree throughout. The new viewer still needs
its own browser checks: snippets with simplified interfaces are not a tested full
application. Author-side checks cover layout/state behavior and exercise the drawing
functions with a recording canvas stub; they do not establish visual quality.
