feat(ui): the search palette, entry points and a trail that survives the URL (CG-45)

Search: `/` or ⌘K focuses the box; results arrive grouped by kind with their
glyph, signature and file:line, ↑/↓/Enter walk them, Esc dismisses. A group
appears where its best result did, so flattening the groups reproduces the
ranking the keyboard walks — the panel's flat item list IS that concatenation.
A flow question ("how does X reach Y", "X -> Y") is recognised and searches
both endpoints with a note, rather than offering a row that would land on the
phase-2 Flow view.

Entry points answer "where do I start" on the empty screen and in the resting
palette, all derived from the graph: routes, files that run something at module
level (the engine records a top-level statement as an edge out of the file node,
which is what makes src/bin/codegraph.ts the root of the CLI flow — ranked by
calls x the files they reach, so a registration table calling into itself does
not outrank the CLI), and the most depended-on symbols. Tests are excluded from
both derived lists.

Trail: hops record the direction they were walked (→ into a call, ← up to a
caller), clicking one truncates back to it, Clear keeps the place instead of
throwing it away, and the whole walk travels in the URL. A shared or reloaded
trail arrives as ids, so hops learn their names back through a new batch
endpoint and a session name cache — without it, walking back across a
truncation redrew earlier hops as raw hashes. "Read as flow" stays hidden until
there is a Flow view to send it to.

New endpoints: /api/entrypoints and /api/nodes. New engine reads:
getTopCallingFiles, getFileDependentCounts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-08-27 00:54:18 -05:00
co-authored by Claude Opus 5
parent e9596af1cf
commit 87afc50e76
20 changed files with 1926 additions and 67 deletions
+62
View File
@@ -1,8 +1,40 @@
<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, 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.
*/
import PaletteRows from '../components/PaletteRows.svelte';
import { palette } from '../lib/palette.svelte';
import { buildEntryPalette, type PaletteItem } from '../lib/search-model';
import { 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) {
const id = item.type === 'route' ? item.nodeId : item.id;
if (!id) 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">
@@ -17,6 +49,15 @@
what it calls on the right — each callee lined up with the line that makes the call.
</p>
</div>
{#if entries.sections.length > 0}
<section class="entries" aria-label="Where to start">
<h3>Where to start</h3>
<div class="rows">
<PaletteRows palette={entries} onpick={pick} />
</div>
</section>
{/if}
</div>
<style>
@@ -24,4 +65,25 @@
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;
}
.entries {
max-width: 720px;
padding: 8px 40px 48px;
}
.entries h3 {
margin: 0 0 8px;
font-size: 14px;
font-weight: 600;
}
.rows {
border: 1px solid var(--rule-soft);
}
</style>