Files
searchi/apps/desktop/src/main.ts
T
karl 81846b3e98
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Canceled after 2m23s
feat: add Google code retrieval across desktop CLI and MCP
2026-10-12 21:22:11 +02:00

1390 lines
54 KiB
TypeScript

import {
invoke,
initializeDesktopSurface,
isNativeDesktop,
isDesktopSurface,
isCaptureSurface,
prepareMediaURL,
} from "./desktop-api";
import { listen } from "@tauri-apps/api/event";
import {
AutomaticIndexGate,
emptySearchState,
LatestLane,
semanticAvailable,
scopedFilters,
SEMANTIC_SETTLE_MS,
} from "./search-state";
import type { SemanticPhase, SearchFilters } from "./search-state";
import { fileIcon, icon } from "./icons";
import {
SearchMeasurement,
measuredFailure,
spanDepth,
briefMs,
} from "./search-performance";
import type { MeasuredResponse, SearchLane } from "./search-performance";
import { displayPath } from "./path-display";
import {
AiRevision,
aiConfigurationProblem,
aiQuestionProblem,
aiEndpointLabel,
aiSourceEligible,
} from "./ai-state";
import type { AiConfiguration } from "./ai-state";
import {
literalQuery,
renderHighlighted,
displayExcerpt,
matchExplanation,
} from "./match-display";
import "@fontsource-variable/inter/wght.css";
import "./style.css";
import {
mediaKind,
matchLabel,
sourceLabel,
seekSeconds,
PreviewRevision,
} from "./media-display";
interface SearchResult {
path: string;
filename: string;
snippet: string;
score: number;
match_kind: string;
source?: { kind: string; number: number; matched_query?: string };
extraction_error?: string;
}
interface IndexReport {
discovered: number;
indexed: number;
removed: number;
skipped: number;
}
interface IndexStats {
files: number;
content_files: number;
}
interface Settings {
version: number;
roots: string[];
cpu_threads: number;
}
interface DesktopStatus {
settings: Settings;
indexing: boolean;
shortcut_status: string;
}
interface SemanticStatus {
model: string;
ready: boolean;
model_directory: string;
worker_ready: boolean;
downloading: boolean;
embedding: boolean;
device_policy: "auto" | "cpu";
error: string | null;
downloaded_bytes: number;
total_bytes: number;
generation: number | null;
chunks: number;
stale: boolean;
pending: number;
warming: boolean;
media_tools_ready?: boolean;
media_setup_error?: string | null;
media_failures?: { path: string; message: string }[];
worker_info?: {
model?: string;
device?: string;
runtime?: string;
threads?: number;
dimensions?: number;
media_supported?: boolean;
} | null;
}
interface SearchRequest {
id: number;
query: string;
filters: SearchFilters;
started: number;
measurement: SearchMeasurement;
}
document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
<header><div class="brand"><img class="brand-symbol" src="/searchi-symbol.png" alt=""><span class="wordmark">searchi</span></div><span id="index-status" class="index-status">Loading your index…</span><div class="header-actions"><button id="add-folder" class="quiet-button" type="button">${icon("folderAdd")}<span>Add folder</span></button><details id="filters" class="popover"><summary>${icon("filter")}<span id="scope-label">All files</span>${icon("chevron")}</summary><div class="popover-panel"><h2>Filter your search</h2><div class="filters-grid"><label>Search scope<select id="filter-scope"><option value="all">All files</option><option value="code">Code</option></select></label><label>Within folder<input id="filter-folder" placeholder="Absolute folder path"></label><label>Extension<input id="filter-extension" placeholder="pdf, md, txt…"></label><label>Minimum size (bytes)<input id="filter-min" type="number" min="0" step="1"></label><label>Maximum size (bytes)<input id="filter-max" type="number" min="0" step="1"></label><label>Minimum duration (seconds)<input id="filter-duration-min" type="number" min="0" step="0.1"></label><label>Maximum duration (seconds)<input id="filter-duration-max" type="number" min="0" step="0.1"></label><label>Modified from<input id="filter-after" type="date"></label><label>Modified through<input id="filter-before" type="date"></label></div></div></details><details id="settings" class="popover"><summary class="icon-button" aria-label="Settings" title="Settings">${icon("settings")}</summary><div class="popover-panel"><h2>Folders & resources</h2><p id="shortcut-status" class="hint"></p><label class="resource-label" for="cpu-threads">CPU thread budget</label><select id="cpu-threads">${Array.from({ length: 8 }, (_, i) => `<option value="${i + 1}">${i + 1} ${i ? "threads" : "thread (default)"}</option>`).join("")}</select><p class="hint">Search automatically uses supported acceleration with CPU fallback.</p><ul id="roots" aria-label="Selected folders"></ul><label class="performance-toggle"><input id="performance-toggle" type="checkbox">Performance details</label><p class="hint">Local timing diagnostics for new searches. Queries and file paths are excluded from copied reports.</p><details id="ai-settings" class="ai-settings"><summary>Optional AI answers</summary><label class="performance-toggle"><input id="ai-enabled" type="checkbox">Enable summaries and questions</label><p class="hint">Disabled by default. Only an explicit action sends the shown passage to your configured endpoint.</p><label>OpenAI-compatible base URL<input id="ai-endpoint" type="url" placeholder="http://localhost:1234/v1" autocomplete="off" maxlength="2048"></label><label>Model<input id="ai-model" placeholder="Your model name" autocomplete="off" maxlength="200"></label><label>API key (optional, this session only)<input id="ai-key" type="password" autocomplete="off" maxlength="2048"></label><button id="ai-clear-key" type="button">Clear key</button><p class="hint">Endpoint, model and key stay in this app session. The key authenticates requests to your endpoint.</p></details></div></details></div></header>
<main>
<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><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><details id="ai-panel" class="ai-panel" hidden><summary>Summarize or ask</summary><p id="ai-disclosure" class="hint"></p><p class="eyebrow">PASSAGE THAT WILL BE SENT</p><pre id="ai-source-text"></pre><div class="ai-actions"><button id="ai-summarize" type="button" disabled>Summarize this passage</button><label class="sr-only" for="ai-question">Question about this passage</label><input id="ai-question" placeholder="Ask about this passage…" maxlength="2048"><button id="ai-ask" type="button" disabled>Ask</button></div><p id="ai-status" class="hint" role="status"></p><pre id="ai-answer"></pre><p id="ai-answer-label" class="hint"></p><button id="ai-citation" type="button" hidden>Selected source</button></details><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 id="privacy-summary">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) =>
document.getElementById(id) as T;
const query = element<HTMLInputElement>("query");
const list = element<HTMLUListElement>("results");
const notice = element<HTMLParagraphElement>("notice");
try {
element<HTMLInputElement>("performance-toggle").checked =
localStorage.getItem("searchi-performance-details") === "true";
} catch {
/* Diagnostics remain opt-in when storage is unavailable. */
}
let results: SearchResult[] = [];
let displayedLiteralQuery = literalQuery("");
let displayedCodeScope = false;
let explainedSelection = "";
let selected = -1;
let requestId = 0;
let timer: ReturnType<typeof setTimeout>;
let indexingActive = false;
let semanticStatus: SemanticStatus | null = null;
let semanticJobActive = false;
let semanticRefreshActive = false;
let semanticTimer: ReturnType<typeof setTimeout> | undefined;
let hybridTimer: ReturnType<typeof setTimeout> | undefined;
let latestSearch: SearchRequest | null = null;
let shownLane: "lexical" | "hybrid" = "lexical";
let folderRevision = 0;
let localSemanticError: string | null = null;
let querySemanticFailed = false;
let previewPath: string | null = null;
const previewRevision = new PreviewRevision();
interface PreviewRequest {
revision: number;
result: SearchResult;
}
function unloadMedia() {
element("media-preview")
.querySelectorAll<HTMLMediaElement>("audio,video")
.forEach((player) => {
player.pause();
player.removeAttribute("src");
player.load();
});
element("media-preview").replaceChildren();
element("media-preview").hidden = true;
element("media-preview-status").hidden = true;
}
function clearMedia() {
previewRevision.clear();
mediaLane.clear();
unloadMedia();
}
const mediaLane = new LatestLane<PreviewRequest>(
async (request) => {
const prepared = await invoke<{
path: string;
kind: "image" | "audio" | "video";
}>("prepare_media_preview", { path: request.result.path });
if (!previewRevision.accepts(request.revision)) return;
const host = element("media-preview");
const status = element("media-preview-status");
const failed = () => {
if (!previewRevision.accepts(request.revision)) return;
status.hidden = false;
status.textContent =
"This format cannot be previewed here. Use Open file to view it in your default application.";
};
if (prepared.kind === "image") {
const image = document.createElement("img");
image.alt = request.result.filename;
image.addEventListener("error", failed, { once: true });
image.src = prepareMediaURL(prepared.path);
host.append(image);
} else {
const player = document.createElement(prepared.kind);
player.controls = true;
player.preload = "metadata";
player.addEventListener("error", failed, { once: true });
player.addEventListener(
"loadedmetadata",
() => {
if (!previewRevision.accepts(request.revision)) return;
const target = seekSeconds(request.result.source);
if (target !== undefined) {
if (!Number.isFinite(player.duration) || target > player.duration) {
status.hidden = false;
status.textContent =
"The matched timestamp is unavailable in this preview. Use Open file to check the recording.";
} else {
try {
player.currentTime = target;
} catch {
failed();
}
}
}
},
{ once: true },
);
player.src = prepareMediaURL(prepared.path);
host.append(player);
}
host.hidden = false;
status.hidden = true;
},
(request) => previewRevision.accepts(request.revision),
(request, error) => {
if (!previewRevision.accepts(request.revision)) return;
element("media-preview-status").hidden = false;
element("media-preview-status").textContent =
`Preview unavailable: ${String(error)}. Use Open file.`;
},
);
let displayedRequestId: number | null = null;
let expandedError: string | null = null;
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
const resultRows = new Map<
string,
{
row: HTMLLIElement;
button: HTMLButtonElement;
title: HTMLElement;
path: HTMLElement;
snippet: HTMLElement;
kind: HTMLElement;
file: HTMLElement;
}
>();
const automaticIndex = new AutomaticIndexGate();
function refreshEmptySearchState() {
if (!latestSearch || latestSearch.id !== requestId || results.length) return;
let phase: SemanticPhase;
if (querySemanticFailed || localSemanticError || semanticStatus?.error)
phase = "unavailable";
else if (
semanticStatus?.embedding ||
semanticJobActive ||
indexingActive ||
(semanticStatus?.ready &&
semanticStatus.stale &&
semanticStatus.pending > 0)
)
phase = "indexing";
else if (!semanticStatus?.ready) phase = "preparing";
else if (semanticStatus.generation === null) phase = "no-content";
else phase = shownLane === "hybrid" ? "complete" : "pending";
const state = emptySearchState(shownLane, phase);
element("empty").querySelector("h2")!.textContent = state.title;
element("empty").querySelector("p")!.textContent = state.detail;
}
function canSearchSemantically() {
return semanticAvailable(semanticStatus);
}
function queueSemanticRefresh(delay = 1000) {
clearTimeout(semanticTimer);
semanticTimer = setTimeout(() => {
void refreshSemanticStatus();
}, delay);
}
async function buildSemanticIndex() {
if (semanticJobActive || indexingActive) return;
semanticJobActive = true;
localSemanticError = null;
element("semantic-status").textContent =
"Building your semantic index… Keyword search remains available.";
element<HTMLProgressElement>("semantic-progress").removeAttribute("value");
element("semantic-progress").hidden = false;
queueSemanticRefresh();
try {
const report = await invoke<{ passages: number; chunks: number }>(
"index_embeddings",
);
element("semantic-status").textContent =
`Semantic search indexed ${report.chunks.toLocaleString()} content segments.`;
} catch (error) {
localSemanticError = String(error);
} finally {
semanticJobActive = false;
queueSemanticRefresh(0);
}
}
async function refreshSemanticStatus() {
if (semanticRefreshActive) {
queueSemanticRefresh();
return;
}
semanticRefreshActive = true;
try {
const previousReady = canSearchSemantically();
const previousGeneration = semanticStatus?.generation;
const status = await invoke<SemanticStatus>("semantic_status");
semanticStatus = status;
refreshEmptySearchState();
const error = localSemanticError || status.error;
if (error && error !== expandedError) {
element<HTMLDetailsElement>("semantic-panel").open = true;
expandedError = error;
}
if (!error) expandedError = null;
const progress = element<HTMLProgressElement>("semantic-progress");
const working =
status.downloading ||
status.warming ||
status.embedding ||
semanticJobActive;
progress.hidden = !working;
progress.removeAttribute("value");
if (
status.downloading &&
status.total_bytes > 0 &&
status.downloaded_bytes < status.total_bytes
) {
progress.max = status.total_bytes;
progress.value = status.downloaded_bytes;
}
const actualDevice =
status.worker_info?.device ||
(status.device_policy === "cpu" ? "CPU" : "Automatic acceleration");
element("privacy").textContent = `Local · ${actualDevice}`;
element("semantic-device").textContent = actualDevice;
let message: string;
if (error) message = error;
else if (status.warming)
message = "Preparing Google semantic search on your device…";
else if (status.downloading)
message =
status.downloaded_bytes >= status.total_bytes
? "Verifying the downloaded Google model…"
: `Downloading Google model · ${Math.round(status.downloaded_bytes / 1_000_000)} of ${Math.round(status.total_bytes / 1_000_000)} MB`;
else if (status.embedding || semanticJobActive)
message =
"Building your semantic index… Keyword search remains available.";
else if (status.ready && status.chunks > 0)
message = `Semantic search ready · ${status.chunks.toLocaleString()} content segments${status.stale ? " · Updates pending" : ""}${status.media_failures?.length ? ` · ${status.media_failures.length} files could not be indexed` : ""}`;
else if (status.ready)
message = "Google model ready. Index a folder to search by meaning.";
else
message =
"Preparing local semantic search… Keyword search remains available.";
element("semantic-status").textContent = message;
element("semantic-status").classList.toggle("error", Boolean(error));
element("semantic-detail").textContent = status.worker_info
? `${status.model} · ${status.worker_info.runtime || "Local inference"} · Your files stay on this device.`
: "The verified Google model downloads automatically once (865 MB). After setup, inference runs locally.";
const mediaDetail = status.media_setup_error
? `Media decoder setup needs attention: ${status.media_setup_error}. Text search remains available.`
: status.worker_info && !status.worker_info.media_supported
? "The installed worker needs a media-capable runtime update. Text search remains available."
: status.media_tools_ready && status.worker_info?.media_supported
? "Image, audio, and video encoders are available locally."
: "Preparing local image, audio, and video support.";
element("semantic-detail").textContent += ` ${mediaDetail}`;
if (status.media_failures?.length) {
element("semantic-detail").textContent +=
` ${status.media_failures.length} files could not be indexed. The first issue: ${status.media_failures[0].path}: ${status.media_failures[0].message}. Filename search remains available; rebuild after fixing the file.`;
}
const retry = element<HTMLButtonElement>("retry-model");
retry.hidden = !error || status.ready;
retry.disabled = working || isCaptureSurface();
const rebuild = element<HTMLButtonElement>("rebuild-semantic");
rebuild.hidden = !status.ready || (!status.chunks && !status.pending);
rebuild.disabled = working || indexingActive || isCaptureSurface();
element<HTMLSelectElement>("cpu-threads").disabled =
working || indexingActive || isCaptureSurface();
if (
!isCaptureSurface() &&
status.ready &&
status.stale &&
status.pending > 0 &&
!working &&
!indexingActive
) {
const key = `${status.generation}:${status.pending}:${folderRevision}`;
if (automaticIndex.claim(key)) {
void buildSemanticIndex();
return;
}
}
if (
(!previousReady || previousGeneration !== status.generation) &&
canSearchSemantically() &&
latestSearch?.id === requestId
)
submitSearch(latestSearch, "hybrid");
if (working || (!status.ready && !error)) queueSemanticRefresh();
} catch (error) {
element("semantic-status").textContent =
`Semantic search unavailable: ${String(error)}`;
element("semantic-status").classList.add("error");
element<HTMLButtonElement>("retry-model").hidden = false;
} finally {
semanticRefreshActive = false;
}
}
async function refreshDesktopStatus() {
const status = await invoke<DesktopStatus>("desktop_status");
indexingActive = status.indexing;
element("shortcut-status").textContent = status.shortcut_status;
element("focus-shortcut").textContent = status.shortcut_status.includes(
"Super",
)
? "Cmd K"
: "Ctrl K";
const threads = element<HTMLSelectElement>("cpu-threads");
threads.value = String(status.settings.cpu_threads);
threads.disabled =
isCaptureSurface() ||
status.indexing ||
semanticJobActive ||
Boolean(
semanticStatus?.downloading ||
semanticStatus?.warming ||
semanticStatus?.embedding,
);
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 = displayPath(root);
path.className = "path";
const reindex = document.createElement("button");
reindex.type = "button";
reindex.textContent = "Reindex";
reindex.disabled = status.indexing || isCaptureSurface();
reindex.addEventListener("click", () => {
element("folder-form").hidden = false;
element<HTMLInputElement>("folder").value = root;
element("folder").focus();
});
item.append(path, reindex);
roots.append(item);
}
if (!status.settings.roots.length) {
const item = document.createElement("li");
item.textContent = "No folders selected yet.";
roots.append(item);
}
if (status.indexing)
element("index-status").textContent =
"Indexing active · Search remains available";
return status;
}
function showError(error: unknown) {
notice.textContent = String(error);
notice.className = "error";
}
interface AiPreview {
id: string;
text: string;
location: string;
}
interface AiAnswer {
text: string;
citations: string[];
model: string;
}
const aiRevision = new AiRevision();
let aiSelectionKey = "";
let aiPrepared: AiPreview | null = null;
let aiRunning = false;
function aiConfiguration(): AiConfiguration {
return {
enabled: element<HTMLInputElement>("ai-enabled").checked,
base_url: element<HTMLInputElement>("ai-endpoint").value.trim(),
model: element<HTMLInputElement>("ai-model").value.trim(),
api_key: element<HTMLInputElement>("ai-key").value || null,
};
}
function clearAiAnswer() {
element("ai-answer").textContent = "";
element("ai-answer-label").textContent = "";
element("ai-citation").hidden = true;
}
function updateAiControls() {
const config = aiConfiguration();
const panel = element<HTMLDetailsElement>("ai-panel");
panel.hidden = !config.enabled;
const result = results[selected];
const eligible = aiSourceEligible(
result?.source,
Boolean(result && mediaKind(result.filename)),
);
const problem = aiConfigurationProblem(config);
const blocked =
!isNativeDesktop() ||
Boolean(problem) ||
!eligible ||
!aiPrepared ||
aiRunning;
element<HTMLButtonElement>("ai-summarize").disabled = blocked;
element<HTMLButtonElement>("ai-ask").disabled =
blocked ||
Boolean(aiQuestionProblem(element<HTMLInputElement>("ai-question").value));
element("ai-disclosure").textContent = !isNativeDesktop()
? "Optional AI actions are available only in the desktop app."
: !eligible
? "Select a result with an indexed text passage to summarize or ask a question."
: problem
? `${problem} Configuration stays in this app session.`
: `Clicking Summarize or Ask sends the exact passage shown below and, for Ask, your question to ${aiEndpointLabel(config.base_url)}. File names and folder paths aren't added. Your session key authenticates that endpoint.`;
element("privacy-summary").textContent =
aiRunning && !config.enabled
? "Local search. An earlier optional AI request is finishing."
: config.enabled
? "Local search. Optional AI shares the shown passage only when requested."
: "Private search. Everything stays on your device.";
}
const aiPreparation = new LatestLane<{
revision: number;
path: string;
source: { kind: string; number: number };
}>(
async (request) => {
const preview = await invoke<AiPreview>("prepare_ai_source", {
enabled: true,
path: request.path,
source: request.source,
revision: request.revision,
});
if (!aiRevision.accepts(request.revision)) return;
aiPrepared = preview;
element("ai-source-text").textContent = preview.text;
element("ai-status").textContent =
`Review the passage beginning from ${preview.location}. Only this shown excerpt will be sent.`;
updateAiControls();
},
(request) => aiRevision.accepts(request.revision),
(_request, error) => {
element("ai-status").textContent = String(error);
updateAiControls();
},
);
function prepareAiSource() {
const result = results[selected];
if (
!aiConfiguration().enabled ||
!isNativeDesktop() ||
!result?.source ||
!aiSourceEligible(result.source, Boolean(mediaKind(result.filename)))
)
return;
element("ai-status").textContent =
"Preparing the selected local passage. Nothing is sent to the endpoint.";
aiPreparation.submit({
revision: aiRevision.current(),
path: result.path,
source: { kind: result.source.kind, number: result.source.number },
});
}
function updateAiSelection(key: string) {
if (aiSelectionKey !== key) {
aiSelectionKey = key;
aiRevision.select(key);
aiPreparation.clear();
aiPrepared = null;
element("ai-source-text").textContent = "";
element<HTMLInputElement>("ai-question").value = "";
element("ai-status").textContent = "";
clearAiAnswer();
updateAiControls();
if (element<HTMLDetailsElement>("ai-panel").open) prepareAiSource();
}
}
async function requestAi(mode: "summary" | "question") {
const config = aiConfiguration();
const question = element<HTMLInputElement>("ai-question").value.trim();
const problem =
aiConfigurationProblem(config) ||
(mode === "question" ? aiQuestionProblem(question) : null);
if (problem || !aiPrepared || !isNativeDesktop() || aiRunning) {
element("ai-status").textContent =
problem || "Prepare and review the current selected passage first.";
return;
}
const revision = aiRevision.current();
const source = aiPrepared;
aiRunning = true;
clearAiAnswer();
updateAiControls();
element("ai-status").textContent =
`Sending the shown passage${mode === "question" ? " and question" : ""} to ${aiEndpointLabel(config.base_url)}…`;
const started = performance.now();
try {
const answer = await invoke<AiAnswer>("generate_ai", {
input: {
enabled: true,
config: {
base_url: config.base_url,
model: config.model,
api_key: config.api_key,
},
source_id: source.id,
mode,
question: mode === "question" ? question : null,
},
});
if (!aiRevision.accepts(revision)) return;
element("ai-answer").textContent = answer.text;
element("ai-answer-label").textContent =
`Model-generated ${answer.citations.length ? "draft" : "uncited draft"} · ${answer.model} · ${briefMs(performance.now() - started)} ms. Verify against the passage.`;
element("ai-status").textContent = "";
const citation = element<HTMLButtonElement>("ai-citation");
citation.hidden = !answer.citations.includes("selected");
citation.textContent = `Source: ${source.location}`;
} catch (error) {
if (aiRevision.accepts(revision))
element("ai-status").textContent = String(error);
} finally {
aiRunning = false;
updateAiControls();
}
}
element("ai-panel").addEventListener("toggle", () => {
if (element<HTMLDetailsElement>("ai-panel").open && !aiPrepared)
prepareAiSource();
});
for (const id of ["ai-enabled", "ai-endpoint", "ai-model", "ai-key"]) {
element(id).addEventListener("input", () => {
aiRevision.invalidate();
aiPreparation.clear();
aiPrepared = null;
clearAiAnswer();
element("ai-source-text").textContent = "";
updateAiControls();
if (element<HTMLDetailsElement>("ai-panel").open) prepareAiSource();
});
}
element("ai-question").addEventListener("input", () => {
aiRevision.invalidate();
clearAiAnswer();
updateAiControls();
if (!aiPrepared && element<HTMLDetailsElement>("ai-panel").open)
prepareAiSource();
});
element("ai-clear-key").addEventListener("click", () => {
element<HTMLInputElement>("ai-key").value = "";
element("ai-key").dispatchEvent(new Event("input"));
});
element("ai-summarize").addEventListener("click", () => {
void requestAi("summary");
});
element("ai-ask").addEventListener("click", () => {
void requestAi("question");
});
element("ai-citation").addEventListener("click", () => {
element("ai-source-text").scrollIntoView({
behavior: reducedMotion.matches ? "auto" : "smooth",
block: "nearest",
});
});
function select(index: number, focus = false) {
if (!results.length) return;
selected = Math.max(0, Math.min(index, results.length - 1));
const result = results[selected];
list.querySelectorAll<HTMLButtonElement>("button").forEach((button, i) => {
button.classList.toggle("selected", i === selected);
button.setAttribute("aria-current", String(i === selected));
button.tabIndex = i === selected ? 0 : -1;
if (i === selected && focus) button.focus();
});
element("preview").hidden = false;
const selectedIcon = fileIcon(result.filename);
element("preview-icon").innerHTML = selectedIcon.html;
element("preview-icon").dataset.type = selectedIcon.type;
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);
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
? "Preview the matched media below."
: "No extracted text is available for this file.");
element("preview-text").classList.toggle("code-passage", displayedCodeScope);
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;
updateAiSelection(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";
const selection = previewRevision.select(
JSON.stringify([result.path, result.source?.kind, result.source?.number]),
);
if (selection.changed) {
mediaLane.clear();
unloadMedia();
if (media && isDesktopSurface()) {
element("media-preview-status").hidden = false;
element("media-preview-status").textContent = "Preparing media preview…";
mediaLane.submit({ revision: selection.revision, result });
}
}
if (previewPath !== result.path) {
const passage = document.querySelector<HTMLElement>(".passage")!;
passage.scrollTop = 0;
if (!reducedMotion.matches) {
passage.getAnimations().forEach((animation) => animation.cancel());
passage.animate(
[
{ opacity: 0.65, transform: "translateY(3px)" },
{ opacity: 1, transform: "translateY(0)" },
],
{ duration: 150, easing: "ease-out" },
);
}
previewPath = result.path;
}
}
function render() {
const scrollTop = list.scrollTop;
const retained = new Set(results.map((result) => result.path));
for (const [key, entry] of resultRows) {
if (!retained.has(key)) {
entry.row.remove();
resultRows.delete(key);
}
}
element("empty").hidden = results.length > 0;
if (!results.length) {
element("preview").hidden = true;
previewPath = null;
clearMedia();
updateAiSelection("");
}
results.forEach((result, i) => {
let entry = resultRows.get(result.path);
if (!entry) {
const row = document.createElement("li");
if (i < 8) row.className = "new-row";
const button = document.createElement("button");
button.className = "result";
button.type = "button";
const file = document.createElement("span");
file.className = "file-icon";
const title = document.createElement("strong");
const path = document.createElement("span");
path.className = "path";
const snippet = document.createElement("span");
snippet.className = "snippet";
const kind = document.createElement("span");
kind.className = "kind";
button.append(file, title, kind, snippet, path);
button.addEventListener("click", () =>
select(Number(button.dataset.index)),
);
row.append(button);
entry = { row, button, file, title, path, snippet, kind };
resultRows.set(result.path, entry);
}
entry.button.dataset.index = String(i);
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);
entry.snippet.classList.toggle("code-snippet", displayedCodeScope);
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) {
entry.file.innerHTML = asset.html;
entry.file.dataset.type = asset.type;
}
const positioned = list.children.item(i);
if (positioned !== entry.row) list.insertBefore(entry.row, positioned);
});
list.scrollTop = scrollTop;
}
function captureFilters() {
const input = (id: string) => element<HTMLInputElement>(id).value.trim();
const size = (id: string) => {
const text = input(id);
if (!text) return null;
const value = Number(text);
if (!Number.isSafeInteger(value) || value < 0)
throw new Error("File size must be a nonnegative whole number of bytes.");
return value;
};
const duration = (id: string) => {
const text = input(id);
if (!text) return null;
const ms = Math.round(Number(text) * 1000);
if (!Number.isFinite(ms) || ms < 0 || ms > 4_294_967_295)
throw new Error("Enter a valid nonnegative media duration in seconds.");
return ms;
};
// Date controls represent the user's local calendar day, including its end.
const date = (id: string, end: boolean) => {
const text = input(id);
if (!text) return null;
const stamp = new Date(
`${text}T${end ? "23:59:59.999" : "00:00:00"}`,
).getTime();
if (!Number.isFinite(stamp))
throw new Error("Enter a valid modification date.");
return stamp;
};
return scopedFilters(element<HTMLSelectElement>("filter-scope").value, {
folder: input("filter-folder") || null,
extension: input("filter-extension") || null,
min_duration_ms: duration("filter-duration-min"),
max_duration_ms: duration("filter-duration-max"),
min_size: size("filter-min"),
max_size: size("filter-max"),
modified_after_unix_ms: date("filter-after", false),
modified_before_unix_ms: date("filter-before", true),
});
}
function applyResults(
found: SearchResult[],
request: SearchRequest,
lane: "lexical" | "hybrid",
) {
if (
request.id !== requestId ||
(lane === "lexical" && shownLane === "hybrid")
)
return;
const renderStarted = performance.now();
const selectedPath = results[selected]?.path;
const resultsHadFocus = list.contains(document.activeElement);
shownLane = lane;
displayedRequestId = request.id;
displayedLiteralQuery = literalQuery(request.query);
displayedCodeScope = request.filters.code_only === true;
results = found;
selected = -1;
render();
const preserved = results.findIndex((result) => result.path === selectedPath);
if (preserved >= 0) select(preserved, resultsHadFocus);
else if (results.length) select(0, resultsHadFocus);
if (!found.length) refreshEmptySearchState();
request.measurement.displayed(
lane,
renderStarted,
performance.now(),
found.length,
);
const metric = request.measurement.latestNative(lane);
const trace = request.measurement.export();
element("result-summary").textContent =
`${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);
}
function refreshPerformance(request: SearchRequest) {
if (request.id !== requestId) return;
const panel = element<HTMLDetailsElement>("performance-panel");
panel.hidden = !request.measurement.debug;
if (!request.measurement.debug) return;
const body = element("performance-detail");
body.replaceChildren();
const report = request.measurement.export();
const append = (text: string, className = "") => {
const row = document.createElement("p");
row.textContent = text;
row.className = className;
body.append(row);
return row;
};
append(
"Times below use separate UI and native clocks. Nested spans and parallel lanes must not be added together. DOM update measures synchronous changes, not paint or deferred media preview loading.",
"hint",
);
append(
`Input debounce ${report.debounce_ms.toFixed(1)} ms · First displayed response ${report.first_results_ms?.toFixed(1) ?? "pending"} ms · Latest displayed ${report.latest_displayed_ms?.toFixed(1) ?? "pending"} ms · Hybrid final ${report.hybrid_final_results_ms?.toFixed(1) ?? "pending / unavailable"} ms`,
);
for (const [lane, delay] of Object.entries(report.lane_schedule_ms))
append(
`${lane}: scheduled ${delay.toFixed(1)} ms after debounce${lane === "hybrid" ? ` (includes intentional ${SEMANTIC_SETTLE_MS} ms semantic delay or waiting for readiness)` : ""}`,
);
for (const attempt of report.attempts) {
append(
`${attempt.lane} attempt ${attempt.attempt}${attempt.failed ? " · FAILED" : ""}: pending queue ${attempt.queued_ms.toFixed(1)} ms · retry wait ${attempt.retry_wait_ms.toFixed(1)} ms · invoke ${attempt.invoke_ms.toFixed(1)} ms`,
"performance-attempt",
);
if (!attempt.native) {
append("Native timing unavailable for this failure.", "hint");
continue;
}
append(
`Native command ${attempt.native.total_ms.toFixed(1)} ms · ${attempt.native.result_count} results · estimated transport / serialization / scheduling ${attempt.transport_estimate_ms?.toFixed(1)} ms`,
"hint",
);
if (attempt.native.trace === null)
append("Detailed native trace unavailable for this attempt.", "hint");
const spans = attempt.native.trace ?? [];
spans.forEach((span, index) => {
const counts = Object.entries(span.counters)
.map(([name, value]) =>
name.endsWith("_us")
? `${name.slice(0, -3).replaceAll("_", " ")}: ${(value / 1000).toFixed(3)} ms`
: `${name.replaceAll("_", " ")}: ${value}`,
)
.join(" · ");
const row = append(
`${span.name}: ${span.duration_ms.toFixed(2)} ms · starts +${span.start_ms.toFixed(2)} ms${counts ? ` · ${counts}` : ""}`,
"performance-span",
);
row.style.paddingLeft = `${spanDepth(spans, index) * 14}px`;
});
if (attempt.native.dropped_spans)
append(`${attempt.native.dropped_spans} native spans dropped.`, "hint");
}
for (const display of report.displays)
append(
`${display.lane} synchronous DOM update ${display.dom_update_ms.toFixed(2)} ms · ${display.count} displayed results`,
"hint",
);
if (report.dropped_attempts || report.dropped_displays)
append(
`History capped: ${report.dropped_attempts} attempts and ${report.dropped_displays} displays dropped.`,
"hint",
);
}
async function invokeMeasured(request: SearchRequest, lane: SearchLane) {
const attempt = request.measurement.begin(lane, performance.now());
try {
const response = await invoke<MeasuredResponse<SearchResult>>(
lane === "lexical" ? "search_measured" : "hybrid_search_measured",
{
query: request.query,
filters: request.filters,
debugPerformance: request.measurement.debug,
},
);
request.measurement.completed(
attempt,
performance.now(),
response.metrics,
false,
);
return response.results;
} catch (error) {
const failure = measuredFailure(error);
request.measurement.completed(
attempt,
performance.now(),
failure?.metrics ?? null,
true,
);
refreshPerformance(request);
throw failure ? new Error(failure.message) : error;
}
}
function submitSearch(request: SearchRequest, lane: SearchLane) {
request.measurement.submitted(lane, performance.now());
(lane === "lexical" ? lexicalLane : hybridLane).submit(request);
}
const isCurrent = (request: SearchRequest) => request.id === requestId;
const lexicalLane = new LatestLane<SearchRequest>(
async (request) => {
const found = await invokeMeasured(request, "lexical");
applyResults(found, request, "lexical");
},
isCurrent,
(_request, error) => {
if (shownLane !== "hybrid") {
showError(error);
element("result-summary").textContent = "KEYWORD SEARCH UNAVAILABLE";
}
},
);
const hybridLane = new LatestLane<SearchRequest>(
async (request) => {
if (!canSearchSemantically()) return;
const found = await invokeMeasured(request, "hybrid");
applyResults(found, request, "hybrid");
},
isCurrent,
(_request, error) => {
querySemanticFailed = true;
refreshEmptySearchState();
element("semantic-status").textContent =
`Semantic results unavailable: ${String(error)}. Keyword results remain available.`;
queueSemanticRefresh();
},
);
function invalidateSearch() {
++requestId;
clearTimeout(timer);
clearTimeout(hybridTimer);
lexicalLane.clear();
hybridLane.clear();
latestSearch = null;
}
async function search(inputAt = performance.now()) {
invalidateSearch();
querySemanticFailed = false;
shownLane = "lexical";
const value = query.value.trim();
if (!value) {
element("performance-panel").hidden = true;
results = [];
displayedLiteralQuery = literalQuery("");
displayedRequestId = null;
selected = -1;
render();
element("result-summary").textContent = "READY WHEN YOU ARE";
element("empty").querySelector("h2")!.textContent =
"Find what matters. Locally.";
element("empty").querySelector("p")!.textContent =
"Index a folder, then search a filename, phrase, or what you remember.";
return;
}
try {
const request: SearchRequest = {
id: requestId,
query: value,
filters: captureFilters(),
started: performance.now(),
measurement: new SearchMeasurement(
inputAt,
performance.now(),
element<HTMLInputElement>("performance-toggle").checked,
{
model: semanticStatus?.worker_info?.model ?? null,
device: semanticStatus?.worker_info?.device ?? null,
runtime: semanticStatus?.worker_info?.runtime ?? null,
threads: semanticStatus?.worker_info?.threads ?? null,
dimensions: semanticStatus?.worker_info?.dimensions ?? null,
semantic_ready: canSearchSemantically(),
indexed_chunks: semanticStatus?.chunks ?? null,
},
),
};
latestSearch = request;
refreshPerformance(request);
element("result-summary").textContent = "SEARCHING…";
submitSearch(request, "lexical");
hybridTimer = setTimeout(() => {
if (isCurrent(request) && canSearchSemantically())
submitSearch(request, "hybrid");
}, SEMANTIC_SETTLE_MS);
} catch (error) {
showError(error);
}
}
async function refreshStats() {
const stats = await invoke<IndexStats>("stats");
element("index-status").textContent =
`${stats.files.toLocaleString()} files · ${stats.content_files.toLocaleString()} with extracted text`;
}
query.addEventListener("input", () => {
const inputAt = performance.now();
invalidateSearch();
timer = setTimeout(() => void search(inputAt), 60);
});
element("performance-toggle").addEventListener("change", () => {
const enabled = element<HTMLInputElement>("performance-toggle").checked;
try {
localStorage.setItem("searchi-performance-details", String(enabled));
} catch {
/* Session preference still works. */
}
void search();
});
element("copy-performance").addEventListener("click", () => {
if (!latestSearch?.measurement.debug) return;
if (!navigator.clipboard) {
element("copy-performance-status").textContent =
"Clipboard unavailable. Timings remain visible here.";
return;
}
const report = JSON.stringify(latestSearch.measurement.export(), null, 2);
void navigator.clipboard.writeText(report).then(
() => {
element("copy-performance-status").textContent =
"Timing report copied. No query or file paths included.";
},
() => {
element("copy-performance-status").textContent =
"Clipboard unavailable. Timings remain visible here.";
},
);
});
element("search-form").addEventListener("submit", (event) => {
event.preventDefault();
clearTimeout(timer);
void search();
});
element("clear-query").addEventListener("click", () => {
query.value = "";
void search();
query.focus();
});
document.querySelectorAll<HTMLDetailsElement>(".popover").forEach((popover) => {
popover.addEventListener("toggle", () => {
if (popover.open)
document
.querySelectorAll<HTMLDetailsElement>(".popover")
.forEach((other) => {
if (other !== popover) other.open = false;
});
});
});
document.addEventListener("pointerdown", (event) => {
document
.querySelectorAll<HTMLDetailsElement>(".popover[open]")
.forEach((popover) => {
if (!popover.contains(event.target as Node)) popover.open = false;
});
});
element("add-folder").addEventListener("click", () => {
element("folder-form").hidden = false;
element("folder").focus();
});
element("cancel-folder").addEventListener("click", () => {
element("folder-form").hidden = true;
query.focus();
});
element("browse-folder").addEventListener("click", async () => {
try {
const folder = await invoke<string | null>("pick_folder");
if (folder) element<HTMLInputElement>("folder").value = folder;
} catch (error) {
showError(error);
}
});
element("filters").addEventListener("change", () => {
element("scope-label").textContent =
element<HTMLSelectElement>("filter-scope").value === "code"
? "Code"
: "All files";
void search();
});
element("retry-model").addEventListener("click", async () => {
const button = element<HTMLButtonElement>("retry-model");
button.disabled = true;
localSemanticError = null;
element("semantic-status").textContent = "Retrying Google model setup…";
queueSemanticRefresh();
try {
await invoke("setup_model");
} catch (error) {
localSemanticError = String(error);
} finally {
button.disabled = false;
queueSemanticRefresh(0);
}
});
element("rebuild-semantic").addEventListener("click", () => {
void buildSemanticIndex();
});
async function openSelected(reveal: boolean) {
if (selected < 0 || !results[selected]) return;
try {
await invoke("open_result", { path: results[selected].path, reveal });
} catch (error) {
showError(error);
}
}
element("open-result").addEventListener("click", () => {
void openSelected(false);
});
element("reveal-result").addEventListener("click", () => {
void openSelected(true);
});
element("cpu-threads").addEventListener("change", async () => {
try {
await invoke<Settings>("set_cpu_threads", {
threads: Number(element<HTMLSelectElement>("cpu-threads").value),
});
await refreshDesktopStatus();
queueSemanticRefresh(0);
} catch (error) {
showError(error);
await refreshDesktopStatus().catch(showError);
}
});
element("folder-form").addEventListener("submit", async (event) => {
event.preventDefault();
const input = element<HTMLInputElement>("folder");
const buttons =
element("folder-form").querySelectorAll<HTMLButtonElement>("button");
buttons.forEach((button) => {
button.disabled = true;
});
element<HTMLButtonElement>("add-folder").disabled = true;
element<HTMLSelectElement>("cpu-threads").disabled = true;
indexingActive = true;
element("index-status").textContent =
"Indexing active · Search remains available";
notice.className = "";
notice.textContent = "Indexing folder… Search remains available.";
try {
const report = await invoke<IndexReport>("index_folder", {
root: input.value.trim(),
});
notice.textContent = `Indexed ${report.indexed} files. ${report.removed} removed; ${report.skipped} skipped.`;
element("folder-form").hidden = true;
folderRevision++;
localSemanticError = null;
await refreshStats();
await search();
} catch (error) {
showError(error);
} finally {
buttons.forEach((button) => {
button.disabled = false;
});
element<HTMLButtonElement>("add-folder").disabled = false;
await refreshDesktopStatus().catch(showError);
await refreshStats().catch(showError);
queueSemanticRefresh(0);
}
});
document.addEventListener("keydown", (event) => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
event.preventDefault();
query.focus();
query.select();
}
if (event.key === "Escape") {
clearMedia();
document
.querySelectorAll<HTMLDetailsElement>(".popover[open]")
.forEach((popover) => {
popover.open = false;
});
++requestId;
query.value = "";
void search();
query.focus();
}
if (event.target === query || list.contains(event.target as Node)) {
if (
event.key === "Enter" &&
results[selected] &&
displayedRequestId === requestId
) {
event.preventDefault();
void openSelected(false);
}
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
event.preventDefault();
select(selected + (event.key === "ArrowDown" ? 1 : -1), true);
}
}
});
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",
"ai-enabled",
"ai-endpoint",
"ai-model",
"ai-key",
]) {
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);
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);
});