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

+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);
});