Files
searchi/apps/desktop/src/main.ts
T

1991 lines
77 KiB
TypeScript
Raw Normal View History

import {
invoke,
initializeDesktopSurface,
isNativeDesktop,
isDesktopSurface,
isCaptureSurface,
prepareMediaURL,
} from "./desktop-api";
import { appearanceView, AppearanceUpdates } from "./appearance-state";
import type { AppearanceStatus, BackgroundMaterial } from "./appearance-state";
import { listen } from "@tauri-apps/api/event";
import {
AutomaticIndexGate,
emptySearchState,
LatestLane,
semanticAvailable,
scopedFilters,
hasSearchInput,
SEMANTIC_SETTLE_MS,
} from "./search-state";
import type { SemanticPhase, SearchFilters } from "./search-state";
import {
semanticIndexReady,
semanticIndexKey,
semanticRetryVisible,
semanticMediaReady,
} from "./semantic-setup-state";
import type { SemanticCoverage } from "./semantic-setup-state";
import { fileIcon, icon } from "./icons";
import {
SearchMeasurement,
measuredFailure,
spanDepth,
briefMs,
} from "./search-performance";
import type {
MeasuredResponse,
SearchLane,
SearchRefreshTrigger,
} from "./search-performance";
import { displayPath } from "./path-display";
import { ResultSelection } from "./selection-state";
import {
AiRevision,
aiConfigurationProblem,
aiQuestionProblem,
aiEndpointLabel,
aiSourceEligible,
} from "./ai-state";
import type { AiConfiguration } from "./ai-state";
import {
literalQuery,
renderHighlighted,
displayExcerpt,
matchExplanation,
baseMatchKind,
partitionResults,
} from "./match-display";
import "@fontsource-variable/inter/wght.css";
import "./style.css";
import {
mediaKind,
matchLabel,
sourceLabel,
seekSeconds,
PreviewRevision,
previewSelectionKey,
} 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;
automatic_indexing?: boolean;
background_material?: BackgroundMaterial;
background_opacity?: number;
}
interface DesktopStatus {
settings: Settings;
indexing: boolean;
shortcut_status: string;
appearance?: AppearanceStatus;
watching?: {
root: string;
attached: boolean;
pending: boolean;
error: string | null;
watch_error: string | null;
}[];
}
interface SpeechStatus {
worker_ready: boolean;
ready: boolean;
downloading: boolean;
downloaded_bytes: number;
total_bytes: number;
error: string | null;
requested_policy: string | null;
pending: boolean;
corpus_revision: number;
failures: number;
}
interface SemanticStatus {
speech?: SpeechStatus;
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;
text_model_ready?: boolean;
media_model_ready?: boolean;
media_model_error?: string | null;
indexing_ready?: boolean;
requested_coverage?: SemanticCoverage;
coverage?: SemanticCoverage | null;
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 id="launcher-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"><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>Artist (exact)<input id="filter-artist" placeholder="Artist from music tags" maxlength="256"></label><label>Track title (exact)<input id="filter-title" placeholder="Title from music tags" maxlength="256"></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>Settings</h2><section class="appearance-settings" aria-labelledby="appearance-heading"><h3 id="appearance-heading">Appearance</h3><label for="background-material">Window background</label><select id="background-material"><option value="solid">Solid</option><option value="glass">Glass</option><option value="frosted">Frosted glass</option></select><div class="appearance-opacity"><label for="background-opacity">Background opacity</label><output id="background-opacity-value" for="background-opacity">90%</output></div><input id="background-opacity" type="range" min="40" max="100" step="1" value="90" aria-describedby="appearance-status"><p id="appearance-status" class="hint" role="status">A subtle window background. Search results and passages stay clear.</p></section><h3>Folders & resources</h3><label class="performance-toggle"><input id="automatic-indexing" type="checkbox" checked>Search my device automatically</label><p class="hint">Your local drives are indexed in the background. Existing results remain searchable when this is off.</p><button id="add-folder" class="quiet-button" type="button">${icon("folderAdd")}<span>Add folder</span></button><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 === 1 ? "threads (default)" : i ? "threads" : "thread (low power)"}</option>`).join("")}</select><p class="hint">Search automatically uses supported acceleration with CPU fallback. Two or more threads let search run alongside indexing; one thread can delay semantic search during indexing.</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="
<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><p id="speech-status" class="hint" role="status" hidden></p><progress id="speech-progress" aria-label="Local speech model setup progress" hidden></progress><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="application-results" aria-label="Applications" hidden></ul><ul id="results" aria-label="Matching files"></ul><details id="related-suggestions" hidden><summary>Related suggestions</summary><ul id="related-results" aria-label="Related file suggestions"></ul></details><div id="empty">${icon("search")}<h2>Find what matters. Locally.</h2><p>Your device is indexed in the background. Search an application, filename, phrase, or what you remember.</p></div></section><aside id="application-preview" aria-label="Application preview" hidden><div class="application-detail"><img src="/searchi-symbol.png" alt="" width="48" height="48"><h2 id="application-name"></h2><p class="hint">Installed application</p><button id="launch-application" type="button">${icon("open")} Launch application</button></div></aside><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>${isNativeDesktop() ? "Hide" : "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");
// Window sizing never delays a search request. Serialize changes so rapid typing
// cannot leave the native window in an older compact/expanded state.
let requestedExpansion = false;
let nativeExpansion: boolean | undefined;
let resizingLauncher = false;
function syncLauncher() {
let filtered = false;
try {
filtered = hasSearchInput(query.value, captureFilters());
} catch {
/* Invalid filters stay editable. */
}
requestedExpansion =
filtered ||
document.querySelector(".popover[open]") !== null ||
element<HTMLDetailsElement>("semantic-panel").open ||
!element("folder-form").hidden;
document.documentElement.dataset.launcherLayout = requestedExpansion
? "expanded"
: "compact";
if (isNativeDesktop() && !resizingLauncher) void resizeLauncher();
}
async function resizeLauncher() {
resizingLauncher = true;
try {
while (nativeExpansion !== requestedExpansion) {
const expanded = requestedExpansion;
await invoke("set_search_layout", { expanded });
nativeExpansion = expanded;
}
} catch (error) {
showError(error);
} finally {
resizingLauncher = false;
}
}
element("launcher-header").addEventListener("pointerdown", (event) => {
if (
isNativeDesktop() &&
event.button === 0 &&
!(event.target as Element).closest("button,summary,input,select,details")
) {
void invoke("drag_search").catch(showError);
}
});
element("hide-launcher").addEventListener("click", () => {
if (isNativeDesktop()) void invoke("hide_search").catch(showError);
else {
query.value = "";
void search();
query.focus();
}
});
element("setup-details").addEventListener("click", () => {
const panel = element<HTMLDetailsElement>("semantic-panel");
panel.open = !panel.open;
});
document
.querySelectorAll<HTMLDetailsElement>(".popover, #semantic-panel")
.forEach((panel) => {
panel.addEventListener("toggle", syncLauncher);
});
new MutationObserver(syncLauncher).observe(element("folder-form"), {
attributes: true,
attributeFilter: ["hidden"],
});
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 allResults: SearchResult[] = [];
const relatedDisclosure = element<HTMLDetailsElement>("related-suggestions");
const relatedList = element<HTMLUListElement>("related-results");
relatedDisclosure
.querySelector("summary")!
.addEventListener("keydown", (event) => {
if (event.key !== "ArrowDown") return;
event.preventDefault();
relatedDisclosure.open = true;
results = partitionResults(allResults, true).visible;
render();
const index = partitionResults(allResults).primary.length;
if (results[index]) select(index, true);
});
relatedDisclosure.addEventListener("toggle", () => {
const previous = results[selected]?.path;
results = partitionResults(allResults, relatedDisclosure.open).visible;
selected = -1;
render();
const retained = previous
? results.findIndex((r) => r.path === previous)
: -1;
if (retained >= 0) select(retained, false, false);
else if (results.length && selectedApplication < 0) select(0, false, false);
else if (selectedApplication < 0) element("preview").hidden = true;
});
interface ApplicationResult {
id: string;
name: string;
}
let applications: ApplicationResult[] = [];
let selectedApplication = -1;
let applicationRevision = 0;
let settledApplicationRevision = -1;
let applicationQuery = "";
function selectApplication(index: number, focus = false) {
if (!applications.length) return;
selectedApplication = Math.max(0, Math.min(index, applications.length - 1));
const app = applications[selectedApplication];
clearMedia();
element("preview").hidden = true;
element("application-preview").hidden = false;
element("application-name").textContent = app.name;
list.querySelectorAll("button").forEach((button) => {
button.classList.remove("selected");
button.setAttribute("aria-current", "false");
});
element("application-results")
.querySelectorAll<HTMLButtonElement>("button")
.forEach((button, i) => {
button.classList.toggle("selected", i === selectedApplication);
button.setAttribute("aria-current", String(i === selectedApplication));
button.tabIndex = i === selectedApplication ? 0 : -1;
if (focus && i === selectedApplication) button.focus();
});
}
async function launchApplication() {
const app = applications[selectedApplication];
if (
!app ||
!isNativeDesktop() ||
applicationQuery !== query.value.trim() ||
settledApplicationRevision !== applicationRevision
)
return;
try {
await invoke("launch_application", { id: app.id });
await invoke("hide_search");
} catch (error) {
showError(error);
}
}
async function refreshApplications() {
if (!isNativeDesktop()) return;
const value = query.value.trim();
const revision = ++applicationRevision;
const changed = value !== applicationQuery;
applicationQuery = value;
if (changed) selectedApplication = -1;
let filtersActive = false;
try {
const filters = captureFilters();
filtersActive = Boolean(filters.code_only) || hasSearchInput("", filters);
} catch {
filtersActive = true;
}
const found =
value && !filtersActive
? await invoke<ApplicationResult[]>("search_applications", {
query: value,
})
: [];
if (revision !== applicationRevision || query.value.trim() !== value) return;
settledApplicationRevision = revision;
applications = found;
const appList = element("application-results");
appList.replaceChildren();
appList.hidden = !found.length;
found.forEach((app, i) => {
const row = document.createElement("li");
const button = document.createElement("button");
button.className = "application-result";
button.type = "button";
const label = document.createElement("strong");
label.textContent = app.name;
const kind = document.createElement("span");
kind.className = "kind";
kind.textContent = "Application";
button.append(label, kind);
button.addEventListener("click", () => selectApplication(i));
button.addEventListener("dblclick", () => {
selectApplication(i);
void launchApplication();
});
row.append(button);
appList.append(row);
});
if (found.length && (changed || selectedApplication >= 0))
selectApplication(selectedApplication < 0 ? 0 : selectedApplication);
else if (!found.length) {
selectedApplication = -1;
element("application-preview").hidden = true;
}
element("empty").hidden = Boolean(found.length || results.length);
updateRelatedEmptyState();
const summary = element("result-summary");
const fileSummary = (summary.textContent ?? "").replace(
/^\d+ APPLICATIONS? � /,
"",
);
summary.textContent = found.length
? `${found.length} APPLICATION${found.length === 1 ? "" : "S"} � ${fileSummary}`
: fileSummary;
}
element("launch-application").addEventListener(
"click",
() => void launchApplication(),
);
element("automatic-indexing").addEventListener("change", async () => {
const checkbox = element<HTMLInputElement>("automatic-indexing");
checkbox.disabled = true;
try {
await invoke("set_automatic_indexing", { enabled: checkbox.checked });
await refreshDesktopStatus();
} catch (error) {
showError(error);
} finally {
checkbox.disabled = !isNativeDesktop();
}
});
let displayedLiteralQuery = literalQuery("");
let displayedCodeScope = false;
let explainedSelection = "";
let selected = -1;
const resultSelection = new ResultSelection();
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 localModelSetupError: 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();
}
function showMediaFailure(request: PreviewRequest, message: string): void {
if (!previewRevision.accepts(request.revision)) return;
const status = element("media-preview-status");
status.hidden = false;
status.textContent = message;
const retry = document.createElement("button");
retry.type = "button";
retry.textContent = "Retry preview";
retry.addEventListener("click", () => {
const revision = previewRevision.retry(request.revision);
if (revision === undefined) return;
unloadMedia();
status.hidden = false;
status.textContent = "Preparing media preview…";
mediaLane.submit({ revision, result: request.result });
});
status.append(" ", retry);
}
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 = () => {
showMediaFailure(
request,
"This preview could not be loaded. Retry or 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) =>
showMediaFailure(
request,
`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 ||
allResults.length ||
applications.length
)
return;
if (!latestSearch.query.trim()) {
element("empty").querySelector("h2")!.textContent =
"No matching music tags.";
element("empty").querySelector("p")!.textContent =
"Try the complete artist or track title from your files. Missing tags cannot match these filters.";
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;
if (semanticStatus && !semanticIndexReady(semanticStatus)) 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 setupError =
status.ready && status.media_model_error ? null : localModelSetupError;
const error = localSemanticError || status.error || setupError;
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.ready && status.text_model_ready
? `${semanticAvailable(status) ? "Text search ready" : "Text model ready"} · Preparing media model · ${Math.round(status.downloaded_bytes / 1_000_000)} of ${Math.round(status.total_bytes / 1_000_000)} MB`
: 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 (semanticAvailable(status) && 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. Your files become searchable by meaning as background indexing completes.";
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."
: semanticMediaReady(status) &&
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_model_error)
element("semantic-detail").textContent +=
` Media model setup needs attention: ${status.media_model_error}. Text search remains available.`;
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 speech = status.speech;
const speechStatus = element("speech-status");
speechStatus.hidden = !speech;
const speechProgress = element<HTMLProgressElement>("speech-progress");
speechProgress.hidden = !speech?.downloading;
if (speech) {
speechProgress.max = speech.total_bytes;
speechProgress.value = speech.downloaded_bytes;
speechStatus.textContent = speech.downloading
? `Preparing local speech passages · ${Math.round(speech.downloaded_bytes / 1_000_000)} of ${Math.round(speech.total_bytes / 1_000_000)} MB`
: speech.error
? `Speech passages: ${speech.error}`
: speech.ready
? speech.failures
? `Speech passages incomplete for ${speech.failures} files. Other search results remain available.`
: speech.pending
? "Speech models ready · Spoken passages are waiting to be indexed."
: "Local speech passages ready. Recognized words and timings can contain errors."
: "Local speech passages are preparing. Filename and existing content search remain available.";
speechStatus.textContent = `Experimental speech search · ${speechStatus.textContent}`;
}
const retry = element<HTMLButtonElement>("retry-model");
retry.hidden =
!semanticRetryVisible({ ...status, error }) && !speech?.error;
retry.disabled =
working || Boolean(speech?.downloading) || isCaptureSurface();
const rebuild = element<HTMLButtonElement>("rebuild-semantic");
rebuild.hidden = !status.ready || (!status.chunks && !status.pending);
rebuild.disabled =
!semanticIndexReady(status) ||
semanticJobActive ||
indexingActive ||
isCaptureSurface();
element<HTMLSelectElement>("cpu-threads").disabled =
working || indexingActive || isCaptureSurface();
if (
!isCaptureSurface() &&
status.ready &&
((status.stale && status.pending > 0) ||
(Boolean(speech?.pending) &&
Boolean(status.generation !== null || status.pending))) &&
semanticIndexReady(status) &&
!semanticJobActive &&
!indexingActive
) {
const key = semanticIndexKey(status, folderRevision);
if (automaticIndex.claim(key)) {
void buildSemanticIndex();
return;
}
}
if (
(!previousReady || previousGeneration !== status.generation) &&
canSearchSemantically() &&
latestSearch?.id === requestId
)
refreshIndexedSearch("semantic_activation");
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;
}
}
const highContrast = window.matchMedia("(forced-colors: active)");
const appearanceUpdates = new AppearanceUpdates(
(update) =>
invoke<AppearanceStatus>(
update.preference ? "set_appearance" : "apply_appearance",
{ ...update.preference, highContrast: update.highContrast },
),
(status) => renderAppearance(status),
showError,
() => {
renderAppearance();
void refreshDesktopStatus(true).catch(showError);
},
);
let lastAppearance: AppearanceStatus | undefined;
function renderAppearance(
status?: AppearanceStatus,
settings?: Settings,
restoreControls = false,
) {
if (status) lastAppearance = status;
const material = element<HTMLSelectElement>("background-material");
const opacity = element<HTMLInputElement>("background-opacity");
if (
!appearanceUpdates.busy &&
settings &&
(restoreControls || document.activeElement !== opacity)
) {
material.value = settings.background_material ?? "frosted";
opacity.value = String(settings.background_opacity ?? 90);
}
const view = appearanceView(lastAppearance, highContrast.matches);
document.documentElement.dataset.backgroundMaterial = view.material;
document.documentElement.style.setProperty(
"--window-opacity",
String(view.opacity / 100),
);
element("background-opacity-value").textContent = `${opacity.value}%`;
material.disabled = !isNativeDesktop() || appearanceUpdates.busy;
opacity.disabled =
!isNativeDesktop() ||
appearanceUpdates.busy ||
material.value === "solid" ||
highContrast.matches;
element("appearance-status").textContent = view.message;
}
function saveAppearance() {
if (!isNativeDesktop()) return;
appearanceUpdates.submit(highContrast.matches, {
material: element<HTMLSelectElement>("background-material")
.value as BackgroundMaterial,
opacity: Number(element<HTMLInputElement>("background-opacity").value),
});
renderAppearance();
}
element("background-material").addEventListener("change", () => {
void saveAppearance();
});
element("background-opacity").addEventListener("input", () => {
const opacity = element<HTMLInputElement>("background-opacity").value;
element("background-opacity-value").textContent = `${opacity}%`;
// Preview only an already confirmed compositor material. Persist on release.
if (!highContrast.matches && lastAppearance?.native_effect) {
document.documentElement.style.setProperty(
"--window-opacity",
String(Number(opacity) / 100),
);
}
});
element("background-opacity").addEventListener("change", () => {
void saveAppearance();
});
highContrast.addEventListener("change", () => {
renderAppearance(); // Fail closed immediately, before the native effect changes.
if (isNativeDesktop()) appearanceUpdates.submit(highContrast.matches);
});
async function refreshDesktopStatus(restoreAppearanceControls = false) {
const appearanceRevision = appearanceUpdates.revision;
const status = await invoke<DesktopStatus>("desktop_status");
if (
!appearanceUpdates.busy &&
appearanceRevision === appearanceUpdates.revision
)
renderAppearance(
status.appearance,
status.settings,
restoreAppearanceControls,
);
indexingActive = status.indexing;
element<HTMLInputElement>("automatic-indexing").checked =
status.settings.automatic_indexing ?? true;
element<HTMLInputElement>("automatic-indexing").disabled = !isNativeDesktop();
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 watched = status.watching?.find((entry) => entry.root === root);
if (watched) {
path.title = watched.error
? `Automatic updates need attention: ${watched.error}`
: watched.pending
? "Checking for file changes…"
: watched.attached
? "Watching for file changes"
: "Checking for changes automatically";
}
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 =
"Searching your device · Ready to search";
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, userChoice = true) {
if (!results.length) return;
selectedApplication = -1;
element("application-preview").hidden = true;
element("application-results")
.querySelectorAll("button")
.forEach((button) => {
button.classList.remove("selected");
button.setAttribute("aria-current", "false");
});
selected = Math.max(0, Math.min(index, results.length - 1));
const result = results[selected];
if (userChoice) resultSelection.choose(result.path);
document
.querySelectorAll<HTMLButtonElement>(
"#results button, #related-results 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(baseMatchKind(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(baseMatchKind(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(
previewSelectionKey(result.path, result.source, folderRevision),
);
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 updateRelatedEmptyState() {
const partition = partitionResults(allResults);
if (
!partition.primary.length &&
partition.related.length &&
!applications.length
) {
element("empty").querySelector("h2")!.textContent = "No strong matches";
element("empty").querySelector("p")!.textContent =
"You can explore the related suggestions.";
}
}
function render() {
const scrollTop = list.scrollTop;
const partition = partitionResults(allResults, relatedDisclosure.open);
relatedDisclosure.hidden = partition.related.length === 0;
relatedDisclosure.querySelector("summary")!.textContent =
`Related suggestions (${partition.related.length})`;
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 || applications.length > 0;
updateRelatedEmptyState();
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(baseMatchKind(result.match_kind));
entry.snippet.classList.toggle("code-snippet", displayedCodeScope);
renderHighlighted(
entry.snippet,
displayExcerpt(result.snippet, displayedLiteralQuery),
semanticMedia ? literalQuery("") : displayedLiteralQuery,
);
entry.kind.textContent = `${matchLabel(baseMatchKind(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 target = result.match_kind.endsWith("_related") ? relatedList : list;
const position = result.match_kind.endsWith("_related")
? i - partition.primary.length
: i;
const positioned = target.children.item(position);
if (positioned !== entry.row) target.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,
media_artist: input("filter-artist") || null,
media_title: input("filter-title") || 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 resultsHadFocus =
list.contains(document.activeElement) ||
relatedList.contains(document.activeElement);
shownLane = lane;
displayedRequestId = request.id;
displayedLiteralQuery = literalQuery(request.query);
displayedCodeScope = request.filters.code_only === true;
allResults = found;
results = partitionResults(found, relatedDisclosure.open).visible;
selected = -1;
render();
const preserved = resultSelection.resolve(
results.map((result) => result.path),
);
if (preserved >= 0 && selectedApplication < 0)
select(preserved, resultsHadFocus, false);
if (!found.length) refreshEmptySearchState();
request.measurement.displayed(
lane,
renderStarted,
performance.now(),
results.length,
);
const metric = request.measurement.latestNative(lane);
const trace = request.measurement.export();
element("result-summary").textContent =
`${partitionResults(found).primary.length ? `${partitionResults(found).primary.length} RESULTS` : found.length ? "NO STRONG MATCHES" : "0 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);
}
function searchMeasurementContext() {
return {
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(),
semantic_generation: semanticStatus?.generation ?? null,
indexed_chunks: semanticStatus?.chunks ?? null,
};
}
function refreshIndexedSearch(
trigger: SearchRefreshTrigger = "filesystem_refresh",
) {
const previous = latestSearch;
if (
!previous ||
previous.id !== requestId ||
query.value.trim() !== previous.query
)
return;
const refreshStarted = performance.now();
clearTimeout(hybridTimer);
lexicalLane.clear();
hybridLane.clear();
querySemanticFailed = false;
shownLane = "lexical";
const refreshed: SearchRequest = {
...previous,
id: ++requestId,
started: refreshStarted,
measurement: previous.measurement.freshForRefresh(
refreshStarted,
trigger,
searchMeasurementContext(),
),
};
latestSearch = refreshed;
submitSearch(refreshed, "lexical");
if (refreshed.query.trim() && canSearchSemantically()) {
hybridTimer = setTimeout(() => {
if (isCurrent(refreshed)) submitSearch(refreshed, "hybrid");
}, SEMANTIC_SETTLE_MS);
}
}
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()) {
syncLauncher();
void refreshApplications().catch(showError);
invalidateSearch();
relatedDisclosure.open = false;
querySemanticFailed = false;
shownLane = "lexical";
const value = query.value.trim();
try {
const filters = captureFilters();
if (!hasSearchInput(value, filters)) {
resultSelection.begin("");
element("performance-panel").hidden = true;
allResults = [];
relatedDisclosure.open = false;
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 =
"Your device is indexed in the background. Search an application, filename, phrase, or what you remember.";
return;
}
resultSelection.begin(JSON.stringify([value, filters]));
const request: SearchRequest = {
id: requestId,
query: value,
filters,
started: performance.now(),
measurement: new SearchMeasurement(
inputAt,
performance.now(),
element<HTMLInputElement>("performance-toggle").checked,
searchMeasurementContext(),
),
};
latestSearch = request;
refreshPerformance(request);
element("result-summary").textContent = "SEARCHING…";
submitSearch(request, "lexical");
hybridTimer = setTimeout(() => {
if (value && 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", () => {
++applicationRevision;
selectedApplication = -1;
applications = [];
element("application-results").replaceChildren();
element("application-results").hidden = true;
element("application-preview").hidden = true;
syncLauncher();
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;
localModelSetupError = null;
element("semantic-status").textContent = "Retrying Google model setup…";
queueSemanticRefresh();
try {
await invoke("setup_model");
} catch (error) {
localModelSetupError = 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") {
if (isNativeDesktop()) {
event.preventDefault();
const popovers =
document.querySelectorAll<HTMLDetailsElement>(".popover[open]");
if (popovers.length) {
popovers.forEach((popover) => {
popover.open = false;
});
} else {
void invoke("hide_search").catch(showError);
}
return;
}
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) ||
relatedList.contains(event.target as Node) ||
element("application-results").contains(event.target as Node)
) {
if (event.key === "Enter" && selectedApplication >= 0) {
event.preventDefault();
void launchApplication();
return;
}
if (
(event.key === "ArrowDown" || event.key === "ArrowUp") &&
applications.length
) {
event.preventDefault();
const step = event.key === "ArrowDown" ? 1 : -1;
const position =
selectedApplication >= 0
? selectedApplication
: applications.length + selected;
const next = Math.max(
0,
Math.min(position + step, applications.length + results.length - 1),
);
if (next < applications.length) selectApplication(next);
else select(next - applications.length);
query.focus();
return;
}
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));
query.focus();
}
}
});
async function initializeSurface() {
await initializeDesktopSurface();
document.documentElement.dataset.nativeLauncher = String(isNativeDesktop());
syncLauncher();
renderAppearance();
if (isNativeDesktop()) {
appearanceUpdates.submit(highContrast.matches);
}
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);
if (isNativeDesktop()) {
void listen("search-hidden", () => {
element("media-preview")
.querySelectorAll<HTMLMediaElement>("audio,video")
.forEach((player) => player.pause());
}).catch(showError);
void listen("folder-indexing-changed", () => {
void refreshDesktopStatus().catch(showError);
queueSemanticRefresh(0);
}).catch(showError);
void listen("search-index-changed", () => {
folderRevision++;
void refreshStats().catch(showError);
void refreshDesktopStatus().catch(showError);
queueSemanticRefresh(0);
refreshIndexedSearch();
}).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);
});