Ask "how does execute reach getFile" in the search box and the viewer draws the call path between them, left to right, opening every card at the exact line that makes the next call. Dynamic-dispatch hops are dashed and name the site they were wired at; "Read as flow" turns a trail walked by hand into the same strip. The path finder is NOT new. `codegraph_explore` already leads its answers with the longest call chain among the symbols an agent named, and a viewer that drew a different path would get the two quoted against each other in a review. So the search moved out of `ToolHandler` into `src/graph/named-symbol-flow.ts` and both callers ride it — same tokens, same overload rules, same synthesized edges. What stayed behind in `tools.ts` is the prose. A pinned from/to question is the same search with two options changed, because both ends being named is the evidence explore's one-unnamed-bridge cap stands in for: it bridges freely, keeps twelve candidates per endpoint instead of six (the CLI's own `main` sorts seventh of ten), and searches from both ends at once — identical paths to the one-way walk on twelve measured pairs, 3-6x faster. `/api/flow` is deliberately the one endpoint with no cache: its cards carry source read from disk, and a drift verdict changes without the index changing. Verified on this repo (`execute` to `rowToFileRecord`, 8 hops; `main` to `resolveOne`, 7) and on a fresh excalidraw index, where `mutateElement` to `renderStaticScene` crosses callback, react-render and jsx-child hops and lists exactly the hops `codegraph_explore` prints. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
156 lines
4.0 KiB
Svelte
156 lines
4.0 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* The rows of a palette — shared by the panel under the search box and the
|
|
* empty screen's "where to start" list, because they are the same rows and a
|
|
* second copy would drift.
|
|
*
|
|
* Selection is passed in rather than owned here: in the panel it belongs to
|
|
* the keyboard, on the empty screen there is none.
|
|
*/
|
|
import KindGlyph from './KindGlyph.svelte';
|
|
import type { Palette, PaletteItem } from '../lib/search-model';
|
|
|
|
interface Props {
|
|
palette: Palette;
|
|
/** Index into `palette.items`, or -1 for no keyboard selection. */
|
|
selected?: number;
|
|
/**
|
|
* Set to 'option' when these rows sit inside a listbox (the search panel).
|
|
* Left off on the empty screen, where they are just links: `role="option"`
|
|
* outside a listbox is a lie a screen reader acts on.
|
|
*/
|
|
rowRole?: 'option' | undefined;
|
|
/** Prefix for each row's DOM id, so a combobox can point at the selected one. */
|
|
idPrefix?: string;
|
|
onpick: (item: PaletteItem) => void;
|
|
onhover?: (index: number) => void;
|
|
}
|
|
|
|
let {
|
|
palette,
|
|
selected = -1,
|
|
rowRole = undefined,
|
|
idPrefix = 'palette-row',
|
|
onpick,
|
|
onhover,
|
|
}: Props = $props();
|
|
|
|
/** Running index into the flat item list, so a row knows its keyboard position. */
|
|
function flatIndex(sectionIndex: number, rowIndex: number): number {
|
|
let base = 0;
|
|
for (let i = 0; i < sectionIndex; i += 1) base += palette.sections[i]?.items.length ?? 0;
|
|
return base + rowIndex;
|
|
}
|
|
</script>
|
|
|
|
{#each palette.sections as section, s (section.title)}
|
|
<div class="head">
|
|
<span class="head-title">{section.title}</span>
|
|
{#if section.note}<span class="head-note">{section.note}</span>{/if}
|
|
</div>
|
|
{#each section.items as item, r (item.id)}
|
|
{@const index = flatIndex(s, r)}
|
|
<button
|
|
type="button"
|
|
class="row"
|
|
class:sel={index === selected}
|
|
data-palette-row={index}
|
|
id={`${idPrefix}-${index}`}
|
|
role={rowRole}
|
|
aria-selected={rowRole ? index === selected : undefined}
|
|
onmousedown={(event) => {
|
|
// mousedown, not click: the input's blur would close the panel first.
|
|
event.preventDefault();
|
|
onpick(item);
|
|
}}
|
|
onmouseenter={() => onhover?.(index)}
|
|
>
|
|
{#if item.type === 'route'}
|
|
<KindGlyph kind="route" />
|
|
<span class="mid">
|
|
<span class="nm">{item.url}</span>
|
|
<span class="sig">{item.handler}</span>
|
|
</span>
|
|
{:else if item.type === 'flow'}
|
|
<KindGlyph kind="route" />
|
|
<span class="mid">
|
|
<span class="nm">{item.name}</span>
|
|
</span>
|
|
{:else}
|
|
<KindGlyph kind={item.node.kind} />
|
|
<span class="mid">
|
|
<span class="nm">{item.name}</span>
|
|
{#if item.meta}<span class="sig">{item.meta}</span>{/if}
|
|
</span>
|
|
{/if}
|
|
<span class="loc">{item.location}</span>
|
|
</button>
|
|
{/each}
|
|
{/each}
|
|
|
|
<style>
|
|
.head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
padding: 6px 10px 4px;
|
|
border-bottom: 1px solid var(--rule-faint);
|
|
color: var(--ink-3);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.head-note {
|
|
overflow: hidden;
|
|
color: var(--ink-4);
|
|
font-size: 11.5px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.row {
|
|
display: grid;
|
|
width: 100%;
|
|
align-items: baseline;
|
|
padding: 6px 10px;
|
|
border-bottom: 1px solid var(--rule-faint);
|
|
color: var(--ink);
|
|
gap: 10px;
|
|
grid-template-columns: 18px 1fr auto;
|
|
text-align: left;
|
|
}
|
|
|
|
.row:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.row:hover,
|
|
.row.sel {
|
|
background: var(--press);
|
|
}
|
|
|
|
.mid {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.nm {
|
|
font-family: var(--mono);
|
|
font-size: 12.5px;
|
|
}
|
|
|
|
.sig {
|
|
margin-left: 6px;
|
|
color: var(--ink-3);
|
|
font-family: var(--mono);
|
|
font-size: 11.5px;
|
|
}
|
|
|
|
.loc {
|
|
color: var(--ink-3);
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|