Files
codegraph/ui/src/views/HomeView.svelte
T
Colby McHenryandClaude Opus 5 47576b392e 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>
2026-08-27 08:31:48 -05:00

145 lines
4.1 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
/**
* The empty screen — and the answer to "where do I start".
*
* Nothing selected is the normal first state of a viewer opened on a project
* nobody has read before, so it carries the same entry points the palette
* shows at rest, at full length: the routes a request arrives on, the files
* that run something at module level, the tests that exercise the most of the
* project, and the symbols the most code depends on. Every one of them is
* derived from the graph — see `src/ui-server/api/entrypoints.ts` for what
* each is derived from.
*
* The full-length version, with the same rows grouped by file and able to
* start a flow, is `#/entry` (`EntryView`); this screen links to it.
*/
import PaletteRows from '../components/PaletteRows.svelte';
import SavedTrails from '../components/SavedTrails.svelte';
import { palette } from '../lib/palette.svelte';
import { buildEntryPalette, type PaletteItem } from '../lib/search-model';
import { entryHref, fileHref, flowHref, navigate } from '../lib/navigation';
import { openEntryTarget, walkTo } from '../lib/walk';
interface Props {
project?: string | null;
}
let { project = null }: Props = $props();
$effect(() => {
void palette.ensureEntries();
});
let entries = $derived(buildEntryPalette(palette.entries));
function pick(item: PaletteItem) {
// The empty screen only ever shows entry points, which are never flows —
// but the row type is shared, so the branch is here rather than assumed away.
if (item.type === 'flow') {
navigate(flowHref({ from: item.from, to: item.to }));
return;
}
if (item.type === 'entry') {
openEntryTarget(item.row.target);
return;
}
const id = item.type === 'route' ? item.nodeId : item.id;
if (!id) return;
// A file opens the File view — its outline plus the import rails. The
// entry-point rows are files far more often than the palette's are.
if (item.type === 'symbol' && item.node.kind === 'file') {
navigate(fileHref(item.node.file));
return;
}
walkTo(
item.type === 'route'
? { id, name: item.handler, kind: null }
: { id, name: item.node.name, kind: item.node.kind },
'start'
);
}
</script>
<div class="scroll">
<div class="emptystate">
<h2>Nothing selected</h2>
<p>
Search for a symbol or a file to start reading{project ? ` in ${project}` : ''}. Press
<code>/</code> to focus the search box.
</p>
<p>
Every symbol you open shows who calls it on the left, its verbatim source in the middle, and
what it calls on the right — each callee lined up with the line that makes the call.
</p>
</div>
<!-- Above the derived lists on purpose: a walk somebody named and kept is a
better place to start than any ranking, when there is one. It draws
nothing at all when there is not. -->
<div class="saved">
<SavedTrails />
</div>
{#if entries.sections.length > 0}
<section class="entries" aria-label="Where to start">
<div class="entries-h">
<h3>Where to start</h3>
<a href={entryHref()}>All entry points </a>
</div>
<div class="rows">
<PaletteRows palette={entries} onpick={pick} />
</div>
</section>
{/if}
</div>
<style>
.scroll {
height: 100%;
overflow: auto;
}
/* `.emptystate` itself is global (app.css) and shared with the other views;
only its bottom padding changes here, to sit against the list below. */
.scroll :global(.emptystate) {
padding-bottom: 8px;
}
.saved {
max-width: 800px;
padding: 8px 40px 0;
}
.entries {
max-width: 720px;
padding: 8px 40px 48px;
}
.entries-h {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
.entries h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.entries-h a {
color: var(--ink-2);
font-size: 12px;
}
.entries-h a:hover {
color: var(--ink);
text-decoration: underline;
}
.rows {
border: 1px solid var(--rule-soft);
}
</style>