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 McHenryandClaude Opus 5 committed 2026-08-27 08:31:48 -05:00
1 parent 55a33055ee
commit 47576b392e
32 files changed
+3162 -78

No files matched your search

+15
View File
@@ -1373,6 +1373,21 @@ export class CodeGraph {
return this.queries.getNodesByIds(ids);
}
/**
* Every symbol carrying an exact qualified name.
*
* The identity that survives a re-index. A node's id contains its start line,
* so any edit ABOVE a symbol gives it a different id — anything that has to
* name the same symbol across two indexes (a saved trail, a bookmark, a
* review comment) has to key on this instead, and then disambiguate the
* result by kind and file. Index-backed; unlike
* {@link GraphQueryManager.findByQualifiedName} it takes no pattern and scans
* nothing.
*/
getNodesByQualifiedName(qualifiedName: string): Node[] {
return this.queries.getNodesByQualifiedNameExact(qualifiedName);
}
/**
* Outgoing edges for many source nodes at once — the batch form of
* {@link getOutgoingEdges}. See {@link QueryBuilder.getOutgoingEdgesFrom}.