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:
co-authored by
Claude Opus 5
parent
55a33055ee
commit
47576b392e
@@ -28,6 +28,7 @@ import {
|
||||
FlowStrip,
|
||||
SearchPalette,
|
||||
SymbolView,
|
||||
SavedTrails,
|
||||
TrailBar,
|
||||
TypeHierarchy,
|
||||
createHttpAdapter,
|
||||
@@ -406,8 +407,22 @@ function mockAdapter(): { adapter: GraphAdapter; calls: string[] } {
|
||||
corroborated: true,
|
||||
timing: { elapsedMs: 1 },
|
||||
}),
|
||||
// Deliberately no `events`: a host without a live channel is the normal
|
||||
// case, and nothing may poll in its absence.
|
||||
trails: () =>
|
||||
seen('trails', {
|
||||
trails: [],
|
||||
// A host with nowhere to keep trails still ANSWERS the question — it
|
||||
// says it is read-only rather than omitting the method, so the screens
|
||||
// show the section explained instead of showing a Save that does
|
||||
// nothing.
|
||||
readOnly: true,
|
||||
readOnlyReason: 'This host does not store trails.',
|
||||
directory: '.codegraph/ui/trails',
|
||||
skipped: 0,
|
||||
bounded: false,
|
||||
}),
|
||||
// Deliberately no `events`, `saveTrail` or `deleteTrail`: a host without a
|
||||
// live channel and without anywhere to write is the normal case, and
|
||||
// nothing may poll or offer to save in their absence.
|
||||
};
|
||||
return { adapter, calls };
|
||||
}
|
||||
@@ -601,6 +616,27 @@ describe('@colbymchenry/codegraph-ui — a host renders the package', () => {
|
||||
expect(host.querySelector('input[role="combobox"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('offers no Save when the adapter cannot write, and says why in the list', async () => {
|
||||
const { adapter } = mockAdapter();
|
||||
setGraphAdapter(adapter);
|
||||
|
||||
trail.push({ id: SYMBOL.node.id, name: 'parseToken', kind: 'function', dir: 'start' });
|
||||
await render(TrailBar, {});
|
||||
// The one screen affordance that must never appear against a read-only
|
||||
// host: an adapter with no `saveTrail` has no button, not a button that
|
||||
// fails.
|
||||
expect(host.textContent ?? '').not.toContain('Save trail');
|
||||
|
||||
void unmount(mounted as Record<string, unknown>);
|
||||
mounted = null;
|
||||
host.innerHTML = '';
|
||||
|
||||
await render(SavedTrails, { hideWhenEmpty: false });
|
||||
const text = host.textContent ?? '';
|
||||
expect(text).toContain('Saved trails');
|
||||
expect(text).toContain('This host does not store trails.');
|
||||
});
|
||||
|
||||
it('CodegraphUi installs the adapter before its children ask for data', async () => {
|
||||
const { adapter, calls } = mockAdapter();
|
||||
// NOT installed by hand — the provider is the only thing that installs it.
|
||||
|
||||
Reference in New Issue
Block a user