feat: highlight search matches and document the working UI
Core CI / core (ubuntu-latest) (push) Failing after 3m29s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 7m23s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s

This commit is contained in:
karl committed 2026-10-12 19:57:34 +02:00
1 parent b8400c4fbf
commit 79553ce921
26 files changed
+4124 -50

No files matched your search

+16
View File
@@ -104,3 +104,19 @@ The local clipboard report excludes queries, paths, filenames, snippets and erro
strings. It captures available model/device/thread/runtime context without
guessing missing values. Histories are bounded and dropped counts remain visible.
Clipboard failure is explicit; no diagnostic telemetry is uploaded.
## Reproducible documentation capture
The optional loopback harness in `tools/desktop-capture` serves this production-built
frontend against the actual shared Rust search engine and local Google model.
Browser activation requires explicit `?capture=1`, a loopback HTTP origin and a
ready `readme-capture` bootstrap with an ephemeral nonce. Read-only search/status
commands use authenticated same-origin requests; indexed media uses restricted
fixture URLs. Indexing, dialogs, external opening and resource-setting mutations
are disabled. Ordinary browser previews remain disconnected and native Tauri
invocations/media URLs retain their existing implementation.
These documentation images show browser-rendered application content backed by
real engine queries. They do not establish native OS chrome, global shortcuts,
native playback or desktop interaction QA. The harness returns no mocked search
results or replayed model output.
+100
View File
@@ -0,0 +1,100 @@
/** Explicit, read-only bridge for documented browser captures of the real engine.
* This never activates in a normal browser preview or remote website. */
const commands = new Set([
"stats",
"desktop_status",
"semantic_status",
"search",
"hybrid_search",
"search_measured",
"hybrid_search_measured",
"prepare_media_preview",
]);
export function captureRequested(href: string) {
const url = new URL(href);
return (
url.searchParams.get("capture") === "1" &&
url.protocol === "http:" &&
["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)
);
}
export class CaptureBridge {
private token: string | null = null;
private origin: string | null = null;
get active() {
return this.token !== null;
}
async initialize(href: string, request: typeof fetch = fetch) {
this.token = null;
this.origin = null;
if (!captureRequested(href)) return false;
const origin = new URL(href).origin;
const response = await request(`${origin}/capture-info`, {
credentials: "omit",
cache: "no-store",
redirect: "error",
});
if (!response.ok)
throw new Error("The local capture engine is unavailable.");
const info = (await response.json()) as {
mode?: unknown;
token?: unknown;
ready?: unknown;
};
if (
info.mode !== "readme-capture" ||
info.ready !== true ||
typeof info.token !== "string" ||
info.token.length < 32 ||
info.token.length > 512
)
throw new Error(
"The local endpoint did not identify a ready read-only capture engine.",
);
this.origin = origin;
this.token = info.token;
return true;
}
async invoke<T>(
command: string,
args?: Record<string, unknown>,
request: typeof fetch = fetch,
): Promise<T> {
if (!this.active || !this.origin)
throw new Error("Run the desktop app to use real local search.");
if (!commands.has(command))
throw new Error(
"This action is unavailable in the read-only documentation capture.",
);
const response = await request(`${this.origin}/capture-api/${command}`, {
method: "POST",
credentials: "omit",
cache: "no-store",
redirect: "error",
headers: {
"Content-Type": "application/json",
"X-Searchi-Capture-Token": this.token!,
},
body: JSON.stringify(args ?? {}),
});
const value = await response.json();
if (!response.ok) throw value;
return value as T;
}
mediaURL(path: string) {
if (
!this.active ||
!this.origin ||
!/^\/capture-media\/[A-Za-z0-9_-]+(?:\.[A-Za-z0-9]+)?$/.test(path)
)
throw new Error(
"Capture media must be an indexed fixture URL issued by the local engine.",
);
return new URL(path, this.origin).href;
}
}
+30
View File
@@ -0,0 +1,30 @@
import {
invoke as nativeInvoke,
isTauri,
convertFileSrc,
} from "@tauri-apps/api/core";
import { CaptureBridge } from "./capture-bridge";
const capture = new CaptureBridge();
export const isNativeDesktop = () => isTauri();
export const isCaptureSurface = () => capture.active;
export const isDesktopSurface = () => isTauri() || capture.active;
export async function initializeDesktopSurface() {
if (isTauri()) return;
await capture.initialize(window.location.href);
}
export function invoke<T>(
command: string,
args?: Record<string, unknown>,
): Promise<T> {
return isTauri()
? nativeInvoke<T>(command, args)
: capture.invoke<T>(command, args);
}
export function prepareMediaURL(path: string) {
if (isTauri()) return convertFileSrc(path);
return capture.mediaURL(path);
}
+140 -45
View File
@@ -1,4 +1,11 @@
import { invoke, isTauri, convertFileSrc } from "@tauri-apps/api/core";
import {
invoke,
initializeDesktopSurface,
isNativeDesktop,
isDesktopSurface,
isCaptureSurface,
prepareMediaURL,
} from "./desktop-api";
import { listen } from "@tauri-apps/api/event";
import {
AutomaticIndexGate,
@@ -14,6 +21,13 @@ import {
briefMs,
} from "./search-performance";
import type { MeasuredResponse, SearchLane } from "./search-performance";
import { displayPath } from "./path-display";
import {
literalQuery,
renderHighlighted,
displayExcerpt,
matchExplanation,
} from "./match-display";
import "@fontsource-variable/inter/wght.css";
import "./style.css";
import {
@@ -95,7 +109,7 @@ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
<form id="search-form" role="search"><label class="sr-only" for="query">Search your files</label>${icon("search")}<input id="query" type="search" placeholder="Search a filename, phrase, or what you remember…" autocomplete="off" spellcheck="false" maxlength="1000"><kbd id="focus-shortcut">Ctrl K</kbd><button id="clear-query" class="icon-button" type="button" aria-label="Clear search" title="Clear search">${icon("dismiss")}</button></form>
<details id="semantic-panel"><summary>${icon("sparkles")}<span id="semantic-status" role="status" aria-live="polite">Preparing local semantic search…</span><span id="semantic-device" class="kind">Local model</span>${icon("chevron")}</summary><div class="semantic-content"><strong>Google EmbeddingGemma 2</strong><progress id="semantic-progress" aria-label="Local model setup progress"></progress><p id="semantic-detail" class="hint">The verified local model downloads automatically on first launch (865 MB). Your files stay on this device.</p><div class="semantic-actions"><button id="retry-model" type="button" hidden>Retry model setup</button><button id="rebuild-semantic" type="button" hidden>Rebuild semantic index</button></div></div></details>
<form id="folder-form" hidden><label for="folder">Folder to index</label><div class="folder-row"><input id="folder" placeholder="Absolute folder path" required><button id="browse-folder" type="button">Browse…</button><button type="submit">Index folder</button><button id="cancel-folder" type="button">Cancel</button></div><p class="hint">Only choose folders you want searched. Large folders may take a while.</p></form>
<p id="notice" role="status" aria-live="polite"></p><details id="performance-panel" hidden><summary>Performance details</summary><div id="performance-detail"></div><button id="copy-performance" type="button">Copy local timing report</button><span id="copy-performance-status" role="status"></span></details><div class="workspace"><section class="results-pane" aria-label="Search results"><p id="result-summary" class="eyebrow">READY WHEN YOU ARE</p><ul id="results" aria-label="Matching files"></ul><div id="empty">${icon("search")}<h2>Find what matters. Locally.</h2><p>Index a folder, then search a filename, phrase, or what you remember.</p></div></section><aside id="preview" aria-label="Result preview" hidden><div class="preview-header"><span id="preview-icon" class="file-icon"></span><div class="preview-heading"><h2 id="preview-title"></h2><p id="preview-path" class="path"></p></div></div><div class="preview-toolbar"><span id="preview-kind"></span><div class="preview-actions"><button id="open-result" type="button">${icon("open")}<span>Open file</span></button><button id="reveal-result" type="button" title="Show in folder" aria-label="Show in folder">${icon("folder")}</button></div></div><div id="media-preview" class="media-preview" hidden></div><p id="media-preview-status" class="hint" role="status" hidden></p><div class="passage"><p id="passage-label" class="eyebrow">EXTRACTED PASSAGE</p><pre id="preview-text"></pre></div></aside></div>
<p id="notice" role="status" aria-live="polite"></p><details id="performance-panel" hidden><summary>Performance details</summary><div id="performance-detail"></div><button id="copy-performance" type="button">Copy local timing report</button><span id="copy-performance-status" role="status"></span></details><div class="workspace"><section class="results-pane" aria-label="Search results"><p id="result-summary" class="eyebrow">READY WHEN YOU ARE</p><ul id="results" aria-label="Matching files"></ul><div id="empty">${icon("search")}<h2>Find what matters. Locally.</h2><p>Index a folder, then search a filename, phrase, or what you remember.</p></div></section><aside id="preview" aria-label="Result preview" hidden><div class="preview-header"><span id="preview-icon" class="file-icon"></span><div class="preview-heading"><h2 id="preview-title"></h2><p id="preview-path" class="path"></p></div></div><div class="preview-toolbar"><span id="preview-kind"></span><div class="preview-actions"><button id="open-result" type="button">${icon("open")}<span>Open file</span></button><button id="reveal-result" type="button" title="Show in folder" aria-label="Show in folder">${icon("folder")}</button></div></div><details id="match-details" class="match-details"><summary>Why this result?</summary><p id="match-reason"></p><p id="match-legend"></p></details><div id="media-preview" class="media-preview" hidden></div><p id="media-preview-status" class="hint" role="status" hidden></p><div class="passage"><p id="passage-label" class="eyebrow">EXTRACTED PASSAGE</p><pre id="preview-text"></pre></div></aside></div>
</main><footer><span class="footer-privacy">${icon("lock")}<span>Private search. Everything stays on your device.</span><span id="privacy" class="privacy">Local</span></span><span class="keyboard-hints"><kbd>${icon("arrowUp")}</kbd><kbd>${icon("arrowDown")}</kbd><span>Navigate</span><kbd>Enter</kbd><span>Open</span><kbd>Esc</kbd><span>Clear</span></span></footer>`;
const element = <T extends HTMLElement>(id: string) =>
@@ -110,6 +124,8 @@ try {
/* Diagnostics remain opt-in when storage is unavailable. */
}
let results: SearchResult[] = [];
let displayedLiteralQuery = literalQuery("");
let explainedSelection = "";
let selected = -1;
let requestId = 0;
let timer: ReturnType<typeof setTimeout>;
@@ -166,7 +182,7 @@ const mediaLane = new LatestLane<PreviewRequest>(
const image = document.createElement("img");
image.alt = request.result.filename;
image.addEventListener("error", failed, { once: true });
image.src = convertFileSrc(prepared.path);
image.src = prepareMediaURL(prepared.path);
host.append(image);
} else {
const player = document.createElement(prepared.kind);
@@ -194,7 +210,7 @@ const mediaLane = new LatestLane<PreviewRequest>(
},
{ once: true },
);
player.src = convertFileSrc(prepared.path);
player.src = prepareMediaURL(prepared.path);
host.append(player);
}
host.hidden = false;
@@ -363,13 +379,14 @@ async function refreshSemanticStatus() {
}
const retry = element<HTMLButtonElement>("retry-model");
retry.hidden = !error || status.ready;
retry.disabled = working;
retry.disabled = working || isCaptureSurface();
const rebuild = element<HTMLButtonElement>("rebuild-semantic");
rebuild.hidden = !status.ready || (!status.chunks && !status.pending);
rebuild.disabled = working || indexingActive;
rebuild.disabled = working || indexingActive || isCaptureSurface();
element<HTMLSelectElement>("cpu-threads").disabled =
working || indexingActive;
working || indexingActive || isCaptureSurface();
if (
!isCaptureSurface() &&
status.ready &&
status.stale &&
status.pending > 0 &&
@@ -411,6 +428,7 @@ async function refreshDesktopStatus() {
const threads = element<HTMLSelectElement>("cpu-threads");
threads.value = String(status.settings.cpu_threads);
threads.disabled =
isCaptureSurface() ||
status.indexing ||
semanticJobActive ||
Boolean(
@@ -418,18 +436,19 @@ async function refreshDesktopStatus() {
semanticStatus?.warming ||
semanticStatus?.embedding,
);
element<HTMLButtonElement>("add-folder").disabled = status.indexing;
element<HTMLButtonElement>("add-folder").disabled =
status.indexing || isCaptureSurface();
const roots = element("roots");
roots.replaceChildren();
for (const root of status.settings.roots) {
const item = document.createElement("li");
const path = document.createElement("span");
path.textContent = root;
path.textContent = displayPath(root);
path.className = "path";
const reindex = document.createElement("button");
reindex.type = "button";
reindex.textContent = "Reindex";
reindex.disabled = status.indexing;
reindex.disabled = status.indexing || isCaptureSurface();
reindex.addEventListener("click", () => {
element("folder-form").hidden = false;
element<HTMLInputElement>("folder").value = root;
@@ -467,21 +486,57 @@ function select(index: number, focus = false) {
const selectedIcon = fileIcon(result.filename);
element("preview-icon").innerHTML = selectedIcon.html;
element("preview-icon").dataset.type = selectedIcon.type;
element("preview-title").textContent = result.filename;
element("preview-path").textContent = result.path;
renderHighlighted(
element("preview-title"),
result.filename,
displayedLiteralQuery,
);
renderHighlighted(
element("preview-path"),
displayPath(result.path),
displayedLiteralQuery,
);
element("preview-kind").textContent =
`${matchLabel(result.match_kind)}${result.source ? ` · ${sourceLabel(result.source)}` : ""}`;
const media = mediaKind(result.filename);
element("passage-label").textContent = media
? "MATCHED CONTENT"
: "EXTRACTED PASSAGE";
element("preview-text").textContent =
const explanation = matchExplanation(result);
const semanticMedia = [
"semantic_image",
"semantic_video",
"semantic_audio",
].includes(result.match_kind);
element("passage-label").textContent = semanticMedia
? "MATCHED MEDIA"
: explanation.semanticPassage
? "MATCHED PASSAGE"
: "EXTRACTED PASSAGE";
const passageText =
result.snippet ||
(result.extraction_error
? `Content could not be extracted: ${result.extraction_error}`
: media
? result.source?.matched_query || "Preview the matched media below."
? "Preview the matched media below."
: "No extracted text is available for this file.");
renderHighlighted(
element("preview-text"),
passageText,
semanticMedia ? literalQuery("") : displayedLiteralQuery,
);
document
.querySelector<HTMLElement>(".passage")!
.classList.toggle("semantic-passage", explanation.semanticPassage);
element("match-reason").textContent = explanation.reason;
element("match-legend").textContent = explanation.legend;
const explanationKey = JSON.stringify([
result.path,
result.match_kind,
result.source?.kind,
result.source?.number,
displayedLiteralQuery.query,
]);
if (explanationKey !== explainedSelection)
element<HTMLDetailsElement>("match-details").open = false;
explainedSelection = explanationKey;
element<HTMLButtonElement>("open-result").title = result.source
? `Open in your default application. The matched location ${sourceLabel(result.source)} is shown in Searchi; default application opening starts at its own location.`
: "Open in your default application";
@@ -491,7 +546,7 @@ function select(index: number, focus = false) {
if (selection.changed) {
mediaLane.clear();
unloadMedia();
if (media && isTauri()) {
if (media && isDesktopSurface()) {
element("media-preview-status").hidden = false;
element("media-preview-status").textContent = "Preparing media preview…";
mediaLane.submit({ revision: selection.revision, result });
@@ -555,10 +610,23 @@ function render() {
resultRows.set(result.path, entry);
}
entry.button.dataset.index = String(i);
entry.button.title = result.path;
entry.title.textContent = result.filename;
entry.path.textContent = result.path;
entry.snippet.textContent = result.snippet;
entry.button.title = displayPath(result.path);
renderHighlighted(entry.title, result.filename, displayedLiteralQuery);
renderHighlighted(
entry.path,
displayPath(result.path),
displayedLiteralQuery,
);
const semanticMedia = [
"semantic_image",
"semantic_video",
"semantic_audio",
].includes(result.match_kind);
renderHighlighted(
entry.snippet,
displayExcerpt(result.snippet, displayedLiteralQuery),
semanticMedia ? literalQuery("") : displayedLiteralQuery,
);
entry.kind.textContent = `${matchLabel(result.match_kind)}${result.source ? ` · ${sourceLabel(result.source)}` : ""}`;
const asset = fileIcon(result.filename);
if (entry.file.dataset.type !== asset.type) {
@@ -627,6 +695,7 @@ function applyResults(
const resultsHadFocus = list.contains(document.activeElement);
shownLane = lane;
displayedRequestId = request.id;
displayedLiteralQuery = literalQuery(request.query);
results = found;
selected = -1;
render();
@@ -643,7 +712,7 @@ function applyResults(
const metric = request.measurement.latestNative(lane);
const trace = request.measurement.export();
element("result-summary").textContent =
`${found.length} RESULTS · ${lane === "hybrid" ? "KEYWORD + MEANING" : "KEYWORD"} · NATIVE ${briefMs(metric?.total_ms ?? 0)} MS · ${lane === "hybrid" ? "FINAL" : "FIRST RESPONSE"} ${Math.round(lane === "hybrid" ? (trace.hybrid_final_results_ms ?? 0) : (trace.first_results_ms ?? 0))} MS`;
`${found.length} RESULTS · ${lane === "hybrid" ? "KEYWORD + MEANING" : "KEYWORD"} · SEARCH ${briefMs(metric?.total_ms ?? 0)} MS · SHOWN ${briefMs(trace.latest_displayed_ms ?? 0)} MS`;
refreshPerformance(request);
}
@@ -801,6 +870,7 @@ async function search(inputAt = performance.now()) {
if (!value) {
element("performance-panel").hidden = true;
results = [];
displayedLiteralQuery = literalQuery("");
displayedRequestId = null;
selected = -1;
render();
@@ -1044,27 +1114,52 @@ document.addEventListener("keydown", (event) => {
}
}
});
if (isTauri()) {
void refreshStats().catch(showError);
void refreshDesktopStatus().catch(showError);
void refreshSemanticStatus();
void listen("focus-search", () => {
query.focus();
query.select();
async function initializeSurface() {
await initializeDesktopSurface();
if (isDesktopSurface()) {
if (isCaptureSurface()) {
for (const id of [
"add-folder",
"open-result",
"reveal-result",
"browse-folder",
"retry-model",
"rebuild-semantic",
"cpu-threads",
]) {
const control = element<HTMLButtonElement>(id);
control.disabled = true;
control.title = "Unavailable in the read-only documentation capture";
}
}
void refreshStats().catch(showError);
void refreshDesktopStatus().catch(showError);
queueSemanticRefresh(0);
}).catch(showError);
// Poll only during a user-triggered job; idle indexing status does no work.
setInterval(() => {
if (indexingActive) void refreshDesktopStatus().catch(showError);
}, 2000);
query.focus();
} else {
element("index-status").textContent = "Desktop preview";
element("semantic-status").textContent =
"Native semantic search runs in the desktop app.";
element("semantic-progress").hidden = true;
element("semantic-detail").textContent =
"This browser preview does not download models or return simulated search results.";
showError("Run the Tauri desktop app to index and search real files.");
void refreshSemanticStatus();
if (isNativeDesktop())
void listen("focus-search", () => {
query.focus();
query.select();
void refreshDesktopStatus().catch(showError);
queueSemanticRefresh(0);
}).catch(showError);
// Poll only during a user-triggered job; idle indexing status does no work.
setInterval(() => {
if (indexingActive) void refreshDesktopStatus().catch(showError);
}, 2000);
query.focus();
} else {
element("index-status").textContent = "Desktop preview";
element("semantic-status").textContent =
"Native semantic search runs in the desktop app.";
element("semantic-progress").hidden = true;
element("semantic-detail").textContent =
"This browser preview does not download models or return simulated search results.";
showError("Run the Tauri desktop app to index and search real files.");
}
}
void initializeSurface().catch((error) => {
element("index-status").textContent = "Capture unavailable";
element("semantic-status").textContent =
"The real local capture engine could not initialize.";
showError(error);
});
+287
View File
@@ -0,0 +1,287 @@
/** Literal text evidence only. Semantic relevance never implies word importance. */
import { timestampLabel } from "./media-display.ts";
const fold = (value: string) =>
value.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
const stopwords = new Set(
"a an and are as at be been but by can could did do does for from had has have how i if in into is it its me my of on or our should so than that the their them there these they this those to was we were what when where which who will with would you your am auch auf aus bei bin bis das dass dem den der des die dieser du ein eine einer einem einen eines er es für hat ich im ist mit nach nicht noch nur oder sein sind so sie sich und uns vom von vor war waren was wie wir wird zu zum zur"
.split(" ")
.map(fold),
);
export interface LiteralQuery {
readonly query: string;
readonly patterns: readonly string[];
}
export interface HighlightPart {
text: string;
matched: boolean;
}
export function literalQuery(value: string): LiteralQuery {
const query = value.slice(0, 4096);
const patterns = new Set<string>();
const add = (text: string) => {
const normalized = fold(text.trim());
if (
normalized &&
!stopwords.has(normalized) &&
/[\p{L}\p{N}]/u.test(normalized) &&
patterns.size < 64
)
patterns.add(normalized);
};
const meaningful = (text: string) =>
[...fold(text).matchAll(/[\p{L}\p{N}]+/gu)].some(
(word) => !stopwords.has(word[0]),
);
// Fixed parsers only: user input is never compiled as a regular expression.
for (const phrase of query.matchAll(/["“]([^"”]+)["”]/gu)) {
if (meaningful(phrase[1])) add(phrase[1]);
}
const unquoted = query.replace(/["“”]/gu, "");
const words = unquoted.split(/\s+/u);
for (const raw of words) {
const word = raw.replace(/^[,;:!?()[\]{}]+|[,;:!?()[\]{}.]+$/gu, "");
add(word);
if (patterns.size >= 64) break;
}
return Object.freeze({
query,
patterns: Object.freeze([...patterns].sort((a, b) => b.length - a.length)),
});
}
export function highlightParts(
text: string,
query: LiteralQuery,
): HighlightPart[] {
if (!text || !query.patterns.length) return [{ text, matched: false }];
const offsets: { start: number; end: number }[] = [];
let normalized = "";
// Index bounded text only; untouched suffixes still preserve the complete source.
for (const segment of text
.slice(0, 65_536)
.matchAll(/[^\p{M}]\p{M}*|\p{M}+/gu)) {
const value = fold(segment[0]);
normalized += value;
for (let index = 0; index < value.length; index++)
offsets.push({
start: segment.index,
end: segment.index + segment[0].length,
});
}
const intervals: { start: number; end: number }[] = [];
for (const pattern of query.patterns) {
let from = 0;
let matches = 0;
while (matches < 256) {
const start = normalized.indexOf(pattern, from);
if (start < 0) break;
const end = start + pattern.length;
const identifier = /[_-]/u.test(pattern);
const left = normalized.slice(Math.max(0, start - 2), start);
const right = normalized.slice(end, end + 2);
const joinedLeft = identifier
? /[\p{L}\p{N}_-]$/u.test(left)
: /[\p{L}\p{N}]$/u.test(left);
const joinedRight = identifier
? /^[\p{L}\p{N}_-]/u.test(right)
: /^[\p{L}\p{N}]/u.test(right);
if (!joinedLeft && !joinedRight && offsets[start] && offsets[end - 1]) {
intervals.push({
start: offsets[start].start,
end: offsets[end - 1].end,
});
matches++;
}
from = end;
}
}
intervals.sort((a, b) => a.start - b.start || b.end - a.end);
const merged: { start: number; end: number }[] = [];
for (const interval of intervals) {
const previous = merged.at(-1);
if (previous && interval.start < previous.end)
previous.end = Math.max(previous.end, interval.end);
else merged.push({ ...interval });
}
const parts: HighlightPart[] = [];
let position = 0;
for (const interval of merged.slice(0, 256)) {
if (position < interval.start)
parts.push({
text: text.slice(position, interval.start),
matched: false,
});
parts.push({
text: text.slice(interval.start, interval.end),
matched: true,
});
position = interval.end;
}
if (position < text.length)
parts.push({ text: text.slice(position), matched: false });
return parts.length ? parts : [{ text, matched: false }];
}
const rendered = new WeakMap<
HTMLElement,
{ text: string; query: LiteralQuery }
>();
const excerpts = new WeakMap<LiteralQuery, Map<string, string>>();
export function displayExcerpt(
text: string,
query: LiteralQuery,
length = 420,
) {
if (text.length <= length) return text;
let cache = excerpts.get(query);
if (!cache) {
cache = new Map();
excerpts.set(query, cache);
}
const key = `${length}:${text}`;
const stored = cache.get(key);
if (stored !== undefined) return stored;
let position = 0;
let firstMatch = 0;
for (const part of highlightParts(text, query)) {
if (part.matched) {
firstMatch = position;
break;
}
position += part.text.length;
}
let start = Math.max(0, firstMatch - 100);
if (start > 0 && /[\uDC00-\uDFFF]/u.test(text[start])) start--;
let end = Math.min(text.length, start + length);
if (end < text.length && /[\uD800-\uDBFF]/u.test(text[end - 1])) end--;
const excerpt = `${start ? "… " : ""}${text.slice(start, end)}${end < text.length ? " …" : ""}`;
if (cache.size >= 128) cache.delete(cache.keys().next().value!);
cache.set(key, excerpt);
return excerpt;
}
export function renderHighlighted(
target: HTMLElement,
text: string,
query: LiteralQuery,
) {
const previous = rendered.get(target);
if (previous?.text === text && previous.query === query) return;
const doc = target.ownerDocument;
const fragment = doc.createDocumentFragment();
for (const part of highlightParts(text, query)) {
if (part.matched) {
const mark = doc.createElement("mark");
mark.dataset.match = "literal";
mark.textContent = part.text;
fragment.append(mark);
} else fragment.append(doc.createTextNode(part.text));
}
target.replaceChildren(fragment);
rendered.set(target, { text, query });
}
interface MatchResult {
match_kind: string;
source?: { kind: string; number: number };
}
export function matchExplanation(result: MatchResult) {
const source = result.source;
const location =
source?.kind === "page"
? ` on page ${source.number}`
: source?.kind === "paragraph"
? ` in paragraph ${source.number}`
: "";
const time =
source?.kind === "timestamp_ms"
? ` near ${timestampLabel(source.number)}`
: "";
const literalLegend =
"Marked words are literal query matches, ignoring case and accents.";
switch (result.match_kind) {
case "semantic_image":
return {
reason:
"Related image content. The match applies to the image as a whole.",
legend: literalLegend,
semanticPassage: false,
};
case "semantic_video":
return {
reason: `Related video frame${time}. The timestamp is the indexed frame position.`,
legend: literalLegend,
semanticPassage: false,
};
case "semantic_audio":
return {
reason: `Related audio${time}. The preview starts at an indexed segment, not an exact spoken phrase.`,
legend: literalLegend,
semanticPassage: false,
};
case "hybrid":
return {
reason: `Query words and related meaning matched this file; showing the passage${location}.`,
legend: `${literalLegend} Meaning can match without the same words.`,
semanticPassage: false,
};
case "semantic":
return {
reason: `Related passage${location}. Similar meaning was found in this passage or its file title.`,
legend: `${literalLegend} Meaning can match without the same words.`,
semanticPassage: true,
};
case "exact_filename":
return {
reason: "The filename matches your query.",
legend: literalLegend,
semanticPassage: false,
};
case "filename":
return {
reason: "Your query appears in the filename.",
legend: literalLegend,
semanticPassage: false,
};
case "path":
return {
reason: "Your query appears in the file path.",
legend: literalLegend,
semanticPassage: false,
};
case "name_terms":
return {
reason: "Query words matched indexed filename, path or content terms.",
legend: literalLegend,
semanticPassage: false,
};
case "media_metadata":
return {
reason: "Query words matched artist, title or other media metadata.",
legend: literalLegend,
semanticPassage: false,
};
case "exact_identifier":
return {
reason: "An exact identifier matched extracted content.",
legend: literalLegend,
semanticPassage: false,
};
case "content":
case "keyword":
return {
reason: `Query words matched extracted content${location}.`,
legend: literalLegend,
semanticPassage: false,
};
default:
return {
reason: "This result was returned by local search.",
legend: literalLegend,
semanticPassage: false,
};
}
}
+6
View File
@@ -0,0 +1,6 @@
/** Explorer-style labels; callers retain the original path for file operations. */
export function displayPath(path: string) {
if (path.startsWith("\\\\?\\UNC\\")) return "\\\\" + path.slice(8);
if (/^\\\\\?\\[A-Za-z]:\\/.test(path)) return path.slice(4);
return path;
}
+45
View File
@@ -836,3 +836,48 @@ footer {
margin-left: 8px;
color: var(--muted);
}
mark[data-match="literal"] {
background: #ffebae;
color: #31280c;
border-radius: 2px;
padding: 0 1px;
font-weight: 600;
box-decoration-break: clone;
}
.passage.semantic-passage {
background: var(--selected);
border-left: 3px solid var(--brand);
}
.match-details {
margin: 0 20px 12px;
color: var(--muted);
font-size: 11px;
}
.match-details summary {
width: fit-content;
cursor: pointer;
}
.match-details p {
margin: 6px 0 0;
line-height: 1.5;
}
#match-legend {
font-size: 10px;
}
@media (prefers-color-scheme: dark) {
mark[data-match="literal"] {
background: #65531e;
color: #fff2bd;
}
}
@media (forced-colors: active) {
mark[data-match="literal"] {
background: Highlight;
color: HighlightText;
outline: 1px solid Highlight;
}
.passage.semantic-passage {
background: Canvas;
border-color: Highlight;
}
}
@@ -0,0 +1,58 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {CaptureBridge, captureRequested} from '../src/capture-bridge.ts';
const ready = () => new Response(JSON.stringify({mode: 'readme-capture', ready: true, token: 'a'.repeat(64)}));
test('ordinary previews and remote pages never initialize a capture bridge', async () => {
const bridge = new CaptureBridge();
let called = 0;
for (const href of ['http://127.0.0.1:1431/', 'https://example.com/?capture=1', 'file:///tmp/demo?capture=1', 'http://127.0.0.1:1431/?capture=0']) {
assert.equal(captureRequested(href), false);
assert.equal(await bridge.initialize(href, async () => { called++; return ready(); }), false);
}
assert.equal(called, 0);
assert.equal(bridge.active, false);
});
test('ready identity and nonce are required before any command dispatch', async () => {
for (const info of [{mode:'other',ready:true,token:'a'.repeat(64)}, {mode:'readme-capture',ready:false,token:'a'.repeat(64)}, {mode:'readme-capture',ready:true,token:'short'}]) {
const bridge = new CaptureBridge();
await assert.rejects(bridge.initialize('http://127.0.0.1:1431/?capture=1', async () => new Response(JSON.stringify(info))));
assert.equal(bridge.active, false);
await assert.rejects(bridge.invoke('stats', {}, async () => assert.fail('Must not dispatch')));
}
});
test('read-only commands use authenticated same-origin requests and mutation is rejected', async () => {
const bridge = new CaptureBridge();
await bridge.initialize('http://127.0.0.1:1431/?capture=1', async () => ready());
let request;
const result = await bridge.invoke('search_measured', {query:'real query'}, async (url, options) => {
request = {url, options}; return new Response(JSON.stringify({results:[],metrics:{total_ms:1}}));
});
assert.equal(result.metrics.total_ms, 1);
assert.equal(request.url, 'http://127.0.0.1:1431/capture-api/search_measured');
assert.equal(request.options.headers['X-Searchi-Capture-Token'], 'a'.repeat(64));
assert.equal(request.options.redirect, 'error');
assert.equal(request.options.credentials, 'omit');
for (const command of ['open_result','index_folder','set_cpu_threads','../../other']) {
await assert.rejects(bridge.invoke(command, {}, async () => assert.fail('Mutation must not dispatch')), /read-only/);
}
});
test('a failed reinitialization cannot retain earlier capture authorization', async () => {
const bridge = new CaptureBridge();
await bridge.initialize('http://127.0.0.1:1431/?capture=1', async () => ready());
await assert.rejects(bridge.initialize('http://127.0.0.1:1431/?capture=1', async () => new Response('{}')));
assert.equal(bridge.active, false);
await assert.rejects(bridge.invoke('stats', {}, async () => assert.fail('No stale authorization')));
});
test('media resolver rejects file paths, remote URLs and traversal; failed measured payload survives', async () => {
const bridge = new CaptureBridge();
await bridge.initialize('http://127.0.0.1:1431/?capture=1', async () => ready());
assert.equal(bridge.mediaURL('/capture-media/cat'), 'http://127.0.0.1:1431/capture-media/cat');
for (const path of ['C:/secret.jpg','https://example.com/cat','//example.com/cat','/capture-media/../secret','/capture-media/%2e%2e','/capture-media/cat?token=other']) assert.throws(() => bridge.mediaURL(path));
await assert.rejects(bridge.invoke('hybrid_search_measured', {}, async () => new Response(JSON.stringify({message:'busy',metrics:{total_ms:4}}), {status:503})), error => error.message === 'busy' && error.metrics.total_ms === 4);
});
+82
View File
@@ -0,0 +1,82 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {literalQuery,highlightParts,renderHighlighted,displayExcerpt,matchExplanation} from '../src/match-display.ts';
const marked = (text,query) => highlightParts(text,literalQuery(query)).filter(part => part.matched).map(part => part.text);
test('literal marks use whole words, identifiers, quoted phrases and filename separators', () => {
assert.deepEqual(marked('Contract contractual subcontract CONTRACT.', 'contract'), ['Contract','CONTRACT']);
assert.deepEqual(marked('E_SEARCH_071 E_SEARCH_0712', 'E_SEARCH_071'), ['E_SEARCH_071']);
assert.deepEqual(marked('invoice_october.pdf', 'invoice october'), ['invoice','october']);
assert.deepEqual(marked('Give end of contract notice. Contract end.', '"end of contract"'), ['end of contract','Contract','end']);
assert.deepEqual(marked('Use C++ and literal a+b, not aab.', 'C++ a+b'), ['C++','a+b']);
});
test('accent and case matching preserves original combining characters and surrogate offsets', () => {
const source='🧾 Café Cafe\u0301 MÜNCHEN München. 𐐀cafe';
const parts=highlightParts(source,literalQuery('cafe munchen'));
assert.equal(parts.map(part => part.text).join(''),source);
assert.deepEqual(parts.filter(part => part.matched).map(part => part.text), ['Café','Cafe\u0301','MÜNCHEN','München']);
assert.equal(parts[0].text, '🧾 ');
});
test('common English/German stopwords and punctuation do not create noisy evidence', () => {
assert.deepEqual(marked('The and für über supplier', 'the and für'), []);
assert.deepEqual(marked('The and. Supplier.', 'the and.'), []);
assert.deepEqual(marked('The supplier partnership may end.', 'cancel the supplier partnership'), ['supplier','partnership']);
});
test('query snapshots and mark runs are bounded; later inputs cannot relabel older results', () => {
let input='supplier';
const snapshot=literalQuery(input);
input='garden';
assert.equal(snapshot.query,'supplier');
assert.deepEqual(highlightParts('supplier garden',snapshot).filter(part => part.matched).map(part => part.text),['supplier']);
assert.equal(Object.isFrozen(snapshot.patterns),true);
assert.ok(literalQuery('x'.repeat(9000)).query.length <= 4096);
assert.ok(literalQuery(Array.from({length:100},(_,i)=>`term${i}`).join(' ')).patterns.length <= 64);
assert.ok(marked('supplier '.repeat(1000),'supplier').length <= 256);
});
test('list excerpts remain original text around the first literal hit and retain ellipsis', () => {
const text='Intro '.repeat(200)+'supplier partnership notice '+ 'Closing '.repeat(200);
const excerpt=displayExcerpt(text,literalQuery('supplier partnership'));
assert.ok(excerpt.length <= 424);
assert.ok(excerpt.startsWith('… '));
assert.ok(excerpt.endsWith(' …'));
assert.ok(excerpt.includes('supplier partnership'));
assert.ok(text.includes(excerpt.slice(2,-2)));
});
test('bounded marks retain the earliest hit across terms instead of favoring longest-term repetition', () => {
const source='short '+ 'longerterm '.repeat(400);
const parts=highlightParts(source,literalQuery('short longerterm'));
assert.equal(parts[0].text,'short');
assert.equal(parts[0].matched,true);
assert.ok(parts.filter(part=>part.matched).length <= 256);
assert.equal(parts.map(part=>part.text).join(''),source);
});
test('markup and regex-looking queries create text nodes and marks, never parsed HTML', () => {
const nodes=[];
const doc={createDocumentFragment:()=>({append:node=>nodes.push(node)}),createElement:tag=>({tag,dataset:{},textContent:''}),createTextNode:text=>({text})};
const target={ownerDocument:doc,replaceChildren:()=>{}};
const source='<img src=x onerror=evil()> supplier [a-z].';
renderHighlighted(target,source,literalQuery('supplier [a-z]'));
assert.equal(nodes.map(node=>node.text ?? node.textContent).join(''),source);
assert.ok(nodes.some(node=>node.tag==='mark' && node.textContent==='supplier'));
assert.ok(nodes.every(node=>node.tag===undefined || node.tag==='mark'));
});
test('optional reasons use real source routes and never claim semantic word confidence', () => {
const text=matchExplanation({match_kind:'semantic',source:{kind:'page',number:2}});
assert.match(text.reason,/page 2/);
assert.match(text.legend,/without the same words/);
assert.equal(text.semanticPassage,true);
assert.match(matchExplanation({match_kind:'semantic_audio',source:{kind:'timestamp_ms',number:13129}}).reason,/0:13.*not an exact spoken phrase/);
assert.match(matchExplanation({match_kind:'semantic_video',source:{kind:'timestamp_ms',number:24167}}).reason,/frame.*0:24/);
assert.match(matchExplanation({match_kind:'semantic_image'}).reason,/whole/);
assert.match(matchExplanation({match_kind:'name_terms'}).reason,/filename, path or content/);
assert.equal(matchExplanation({match_kind:'hybrid',source:{kind:'page',number:2}}).semanticPassage,false);
assert.doesNotMatch(JSON.stringify(text),/confidence|percent|importance/);
});
+1
View File
@@ -6,6 +6,7 @@
"lib": ["ES2022", "DOM"],
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"skipLibCheck": true
},
"include": ["src"]