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:
co-authored by
Claude Opus 5
parent
e9596af1cf
commit
87afc50e76
@@ -163,6 +163,67 @@ export interface WireBlastScale {
|
||||
estimated: boolean;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------- search palette -- */
|
||||
|
||||
/** How a result's text matched the query — the server's primary sort key. */
|
||||
export type MatchKind = 'exact' | 'prefix' | 'substring' | 'qualified' | 'file' | 'related';
|
||||
|
||||
export interface WireSearchResult extends WireNodeRef {
|
||||
matchKind: MatchKind;
|
||||
}
|
||||
|
||||
export interface WireSearchGroup {
|
||||
kind: NodeKind;
|
||||
count: number;
|
||||
items: WireSearchResult[];
|
||||
}
|
||||
|
||||
export interface WireSearch {
|
||||
query: string;
|
||||
/** The free-text part, with any `kind:` / `lang:` / `path:` filters removed. */
|
||||
text: string;
|
||||
filters: { kinds: string[]; languages: string[]; paths: string[]; names: string[] };
|
||||
results: WireList<WireSearchResult>;
|
||||
/** Kind buckets in ranked order — flattening them reproduces the ranking. */
|
||||
groups: WireSearchGroup[];
|
||||
}
|
||||
|
||||
export interface WireNodeRefs {
|
||||
items: WireNodeRef[];
|
||||
/** Ids that name nothing in this index — a stale link, not an error. */
|
||||
missing: string[];
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------- entry points -- */
|
||||
|
||||
export interface WireEntryRoute {
|
||||
url: string;
|
||||
handler: string;
|
||||
file: string;
|
||||
line: number;
|
||||
handlerId: string | null;
|
||||
}
|
||||
|
||||
export interface WireEntryFile extends WireNodeRef {
|
||||
/** Calls and instantiations made at the top level of the file. */
|
||||
calls: number;
|
||||
/** Distinct other files this one's symbols reach. */
|
||||
reaches: number;
|
||||
/** Other files reaching into this one. Zero means nothing imports it. */
|
||||
dependents: number;
|
||||
}
|
||||
|
||||
export interface WireEntryHub extends WireNodeRef {
|
||||
dependents: number;
|
||||
}
|
||||
|
||||
export interface WireEntryPoints {
|
||||
routes: { routed: boolean; routeCount: number; items: WireEntryRoute[] };
|
||||
/** `total` is a floor on both lists — the server counts what its scan saw. */
|
||||
files: WireList<WireEntryFile>;
|
||||
hubs: WireList<WireEntryHub>;
|
||||
}
|
||||
|
||||
export interface WireStats {
|
||||
project: { root: string; name: string };
|
||||
index: {
|
||||
@@ -257,6 +318,33 @@ export function fetchSymbol(id: string, signal?: AbortSignal): Promise<WireSymbo
|
||||
return getJson<WireSymbolPayload>(`api/node/${encoded}`, signal);
|
||||
}
|
||||
|
||||
export function fetchSearch(
|
||||
query: string,
|
||||
opts: { limit?: number } = {},
|
||||
signal?: AbortSignal
|
||||
): Promise<WireSearch> {
|
||||
const params = new URLSearchParams({ q: query });
|
||||
if (opts.limit) params.set('limit', String(opts.limit));
|
||||
return getJson<WireSearch>(`api/search?${params}`, signal);
|
||||
}
|
||||
|
||||
/** Names and locations for ids you already have — what the trail redraws with. */
|
||||
export function fetchNodeRefs(ids: readonly string[], signal?: AbortSignal): Promise<WireNodeRefs> {
|
||||
const params = new URLSearchParams();
|
||||
for (const id of ids) params.append('id', id);
|
||||
return getJson<WireNodeRefs>(`api/nodes?${params}`, signal);
|
||||
}
|
||||
|
||||
export function fetchEntryPoints(
|
||||
opts: { limit?: number } = {},
|
||||
signal?: AbortSignal
|
||||
): Promise<WireEntryPoints> {
|
||||
const params = new URLSearchParams();
|
||||
if (opts.limit) params.set('limit', String(opts.limit));
|
||||
const query = params.toString();
|
||||
return getJson<WireEntryPoints>(`api/entrypoints${query ? `?${query}` : ''}`, signal);
|
||||
}
|
||||
|
||||
export function fetchSource(
|
||||
file: string,
|
||||
from: number,
|
||||
|
||||
Reference in New Issue
Block a user