feat(ui): saved trails — a walk you named, kept, and still true after a re-index (CG-60)

Save trail on the trail bar writes the walk to .codegraph/ui/trails/ as one
JSON file, listed on the empty screen and on Entry points above the derived
suggestions, reopened at the symbol you left with the whole path restored.

A hop is stored by qualified name, kind and file — never by node id, which
contains a start line and so changes the first time anybody edits above the
symbol. Every hop is re-resolved against the current index on the way out and
each row says what became of it: still here, moved to another file, now
ambiguous, or gone. A hole is never stitched over: the row opens the longest
run of CONSECUTIVE resolved hops and says which ones those are, because the
trail is a path and a skipped hop would draw a call that does not exist.

This is the first write the viewer makes, and the boundary moved with it:
POST/DELETE answer under /api/ only, must carry X-CodeGraph-UI and
application/json (neither of which a cross-origin form can produce without a
preflight this server answers none of), and --read-only refuses both while
still listing what is there. The blanket "read-only" claim is retired from the
banner, the README, the CLI help and the docs site in favour of the narrower
true one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-08-27 08:31:48 -05:00
co-authored by Claude Opus 5
parent 55a33055ee
commit 47576b392e
32 changed files with 3162 additions and 78 deletions
+41
View File
@@ -377,6 +377,47 @@ of an ancestor's member, names the language calls by itself, vendored directorie
islands — the Map's job, not this list's), names the resolver failed to resolve somewhere, names shared with a symbol that IS
referenced, and — the only rule that reads a file — names written more than once in a file that can reach them.
### 3.12 Saved trails (CG-60)
A **Save trail** button on the trail bar, and a list of what was saved on the empty screen and the entry-points panel.
The viewer's only write.
**Saving.** `Save trail` sits with `Read as flow` and `Clear` on the right of the trail bar — sans, `4px 8px`,
`--rule-soft` border, same as its neighbours — and appears only once the trail has a hop and the answering side accepts
writes. It opens a **one-field inline form** as a second row inside the bar (never a dialog: naming a walk is a thought the
reader is already having, and anything modal stops the reading to ask about filing). The row is a 12px `--ink-2` sans label,
a **30px** `--paper` input with a `--rule-soft` border exactly like the search box, `Save`/`Cancel`, and an 11.5px hint that
says what will happen *before* it happens: `3 hops · saved to .codegraph/ui/trails`, or, in `--amber`,
`Replaces the saved trail of the same name.` The name is pre-filled with the current symbol's; Escape closes; a failure
(a read-only checkout, a full disk) prints in `--accent` beside the buttons rather than vanishing. The trail bar's grid row
is `auto` for this — it keeps its 34px on its own and grows only while the form is open.
**The list.** Rows follow the search-result grid — `18px | 1fr | auto`, kind glyph of the first hop, name 12.5px mono, then
`N hops · author` 11px mono `--ink-3` — inside a `--rule-soft` box with `--rule-faint` between rows, so the empty screen
reads as one list rather than two. Two 11px `--rule-soft` actions sit at the right of each row, always drawn and receding to
`--ink-3` (a control that appears when the pointer arrives is one a keyboard reader has to guess at): `Export`, and `Delete`
which arms to `Delete?` in `--accent`/`--accent-soft` before it removes anything. The section sits **above** "Where to start":
a walk somebody named beats any ranking, when there is one. It draws nothing at all on the empty screen when there are no
trails, and draws itself explained on the entry-points panel, which is where a reader goes looking for one.
**The honesty line is the feature.** A saved trail is somebody's explanation of code that has since moved, so every hop is
re-resolved against the current index on the way out and each row prints what became of it, in 11.5px under the name:
`--amber` for *"1 hop moved or renamed since this was saved — parseToken no longer in the index."* or *"…now names more than
one symbol — showing the closest match."*, `--ink-3` for a hop that merely moved file. Because the trail is a **path**, a hole
in it cannot be stitched: the row opens the longest run of *consecutive* resolved hops and says so — `Opens hops 24 of 6.`
and a trail where nothing resolves is drawn `--ink-3` and is not clickable.
**Where it lives.** One JSON file per trail under `.codegraph/ui/trails/<slug>.json`, written atomically (temp + rename),
newest save first. `.codegraph/.gitignore` already ignores everything, so a trail is local by default; `Export` downloads the
same file for a reader who wants to commit it somewhere. Each hop is stored as its **qualified name, kind and file** with the
node id kept only as a fast path — a node id contains its start line, so a trail keyed on ids would break the first time
anybody edited the code it describes, which is exactly when it matters. Saving under an existing name replaces that trail and
keeps its `createdAt`.
**What a write has to be.** `POST /api/trails` and `DELETE /api/trails/<id>`, under `/api/` and nowhere else, carrying the
`X-CodeGraph-UI` header and `Content-Type: application/json` — neither of which a cross-origin form can produce without a
CORS preflight this server answers none of. `--read-only` refuses both and the screens say so in the answering side's own
words instead of showing a Save that fails.
## 4. Libraries and versions
- Svelte 5 (≥ 5.25) + Vite (workspace `ui/`), Svelte Flow `@xyflow/svelte` ^1.6 for the Map and Flow canvases only (custom nodes/edges,
hidden handles for port spreading, local selection state — the pattern in docker-app's `StackGraph.svelte`); `@dagrejs/dagre` only as a