From 67d5d06ed5af067deb657c1d55b0e4629a091028 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 18:44:52 +0000 Subject: [PATCH] fix(graph): keyboard-navigable search with separator-tolerant matching The graph viewer's file search only matched literal substrings, so a query like "kb elicit" missed kb-elicit.mjs because the hyphen didn't match the space. It also had no arrow-key navigation through the hit list, so Enter always picked the first result. Add a token-matching fallback tier that normalizes -, _, ., / to spaces before comparing, and wire ArrowUp/ArrowDown to move a highlighted selection that Enter now respects. --- scripts/graph-viewer/sphere.template.html | 31 ++++++++++++++++++++--- src/graph/view-template.ts | 2 +- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/scripts/graph-viewer/sphere.template.html b/scripts/graph-viewer/sphere.template.html index 0e46875..3e1312b 100644 --- a/scripts/graph-viewer/sphere.template.html +++ b/scripts/graph-viewer/sphere.template.html @@ -72,7 +72,8 @@ #hits{max-height:46vh;overflow-y:auto;border-top:1px solid var(--line)} #hits:empty{display:none} .hit{padding:7px 14px;cursor:pointer;font-size:12px;display:flex;align-items:baseline;gap:8px} - .hit:hover{background:rgba(255,212,121,.1)} + .hit:hover,.hit.sel{background:rgba(255,212,121,.1)} + .hit.sel{box-shadow:inset 2px 0 0 var(--accent)} .hit b{font-family:var(--mono);font-weight:500} .hit i{font-style:normal;color:var(--fg-faint);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} @@ -846,15 +847,34 @@

Codebase sphere

document.getElementById('repo').onclick = () => { repoAt = (repoAt + 1) % repoKeys.length; load(repoKeys[repoAt]); }; const q = document.getElementById('q'), hits = document.getElementById('hits'); +// Separators (-, _, ., /) don't mean anything to a human typing a search — +// "kb elicit" should find "kb-elicit.mjs" — so token matching normalizes +// them all to spaces before comparing, on top of the plain substring tiers. +const normSep = s => s.toLowerCase().replace(/[-_./]+/g, ' '); +let selIdx = -1; +function hitEls() { return Array.from(hits.children); } +function setSel(idx) { + const els = hitEls(); + if (!els.length) { selIdx = -1; return; } + selIdx = ((idx % els.length) + els.length) % els.length; + els.forEach((el, j) => el.classList.toggle('sel', j === selIdx)); + els[selIdx].scrollIntoView({ block: 'nearest' }); +} q.addEventListener('input', () => { const v = q.value.trim().toLowerCase(); hits.innerHTML = ''; + selIdx = -1; if (!v) return; + const tokens = v.split(/\s+/).filter(Boolean); const out = []; for (let i = 0; i < M.n; i++) { const f = M.nodes[i][0].toLowerCase(), p = M.nodes[i][1].toLowerCase(); let s = -1; - if (f.startsWith(v)) s = 0; else if (f.includes(v)) s = 1; else if (p.includes(v)) s = 2; + if (f.startsWith(v)) s = 0; + else if (f.includes(v)) s = 1; + else if (p.includes(v)) s = 2; + else if (tokens.length > 1 && tokens.every(t => normSep(f).includes(t))) s = 3; + else if (tokens.every(t => normSep(p).includes(t))) s = 4; if (s >= 0) out.push([s, -M.deg[i], i]); } out.sort((a, b) => a[0] - b[0] || a[1] - b[1]); @@ -865,10 +885,13 @@

Codebase sphere

el.onclick = () => { openFile(i); q.value = ''; hits.innerHTML = ''; q.blur(); }; hits.appendChild(el); } + if (hits.children.length) setSel(0); }); q.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { const f = hits.querySelector('.hit'); if (f) f.click(); } - if (e.key === 'Escape') { q.value = ''; hits.innerHTML = ''; q.blur(); } + if (e.key === 'ArrowDown') { if (hits.children.length) { e.preventDefault(); setSel(selIdx < 0 ? 0 : selIdx + 1); } } + else if (e.key === 'ArrowUp') { if (hits.children.length) { e.preventDefault(); setSel(selIdx < 0 ? hits.children.length - 1 : selIdx - 1); } } + else if (e.key === 'Enter') { const f = hitEls()[selIdx] || hits.querySelector('.hit'); if (f) f.click(); } + else if (e.key === 'Escape') { q.value = ''; hits.innerHTML = ''; selIdx = -1; q.blur(); } }); addEventListener('keydown', (e) => { if (e.key === 'Escape' && mode === 'map') exitMap(); }); diff --git a/src/graph/view-template.ts b/src/graph/view-template.ts index 63157cd..83ff370 100644 --- a/src/graph/view-template.ts +++ b/src/graph/view-template.ts @@ -6,4 +6,4 @@ * `__TITLE__` (the tab title) and `__DATA_JSON__` (the baked graph payload). * Shipped as a string because tsc does not copy non-.ts assets into dist. */ -export const GRAPH_TEMPLATE = "\n\n\n\n\n__TITLE__\n\n\n\n\n\n
\n \n \n

Codebase sphere

\n \n
files indexed—
\n
relations—
\n
directories—
\n
\n\n
\n
\n
centre
\n
\n
\n
\n
opened
\n
\n
\n
\n
\n \n
\n\n
\n \n
\n
\n\n

Directories

\n\n
\n
\n\n\n\n\n"; +export const GRAPH_TEMPLATE = "\n\n\n\n\n__TITLE__\n\n\n\n\n\n
\n \n \n

Codebase sphere

\n \n
files indexed—
\n
relations—
\n
directories—
\n
\n\n
\n
\n
centre
\n
\n
\n
\n
opened
\n
\n
\n
\n
\n \n
\n\n
\n \n
\n
\n\n

Directories

\n\n
\n
\n\n\n\n\n";