2026-10-12 19:16:23 +02:00
import { invoke , isTauri , convertFileSrc } from "@tauri-apps/api/core" ;
2026-10-12 17:44:56 +02:00
import { listen } from "@tauri-apps/api/event" ;
import {
AutomaticIndexGate ,
emptySearchState ,
LatestLane ,
} from "./search-state" ;
import type { SemanticPhase } from "./search-state" ;
import { fileIcon , icon } from "./icons" ;
2026-10-12 19:16:23 +02:00
import {
SearchMeasurement ,
measuredFailure ,
spanDepth ,
briefMs ,
} from "./search-performance" ;
import type { MeasuredResponse , SearchLane } from "./search-performance" ;
2026-10-12 17:44:56 +02:00
import "@fontsource-variable/inter/wght.css" ;
2026-10-12 16:07:53 +02:00
import "./style.css" ;
2026-10-12 19:16:23 +02:00
import {
mediaKind ,
matchLabel ,
sourceLabel ,
seekSeconds ,
PreviewRevision ,
} from "./media-display" ;
2026-10-12 16:07:53 +02:00
interface SearchResult {
path : string ;
filename : string ;
snippet : string ;
score : number ;
match_kind : string ;
2026-10-12 19:16:23 +02:00
source ?: { kind : string ; number : number ; matched_query? : string };
2026-10-12 16:07:53 +02:00
extraction_error? : string ;
}
interface IndexReport {
discovered : number ;
indexed : number ;
removed : number ;
skipped : number ;
}
interface IndexStats {
files : number ;
content_files : number ;
}
2026-10-12 17:44:56 +02:00
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 ;
2026-10-12 19:16:23 +02:00
media_tools_ready? : boolean ;
media_setup_error? : string | null ;
media_failures ?: { path : string ; message : string }[];
2026-10-12 17:44:56 +02:00
worker_info ?: {
model? : string ;
device? : string ;
runtime? : string ;
threads? : number ;
2026-10-12 19:16:23 +02:00
dimensions? : number ;
media_supported? : boolean ;
2026-10-12 17:44:56 +02:00
} | null ;
}
interface SearchRequest {
id : number ;
query : string ;
filters : Record < string , string | number | null >;
started : number ;
2026-10-12 19:16:23 +02:00
measurement : SearchMeasurement ;
2026-10-12 17:44:56 +02:00
}
2026-10-12 16:07:53 +02:00
document . querySelector < HTMLDivElement >( "#app" ) ! . innerHTML = `
2026-10-12 19:16:23 +02:00
<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>All files</span> ${ icon ( "chevron" ) } </summary><div class="popover-panel"><h2>Filter your search</h2><div class="filters-grid"><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></div></details></div></header>
2026-10-12 17:44:56 +02:00
<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>
2026-10-12 16:07:53 +02:00
<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>
2026-10-12 19:16:23 +02:00
<p id="notice" role="status" aria-live="polite"></p><details id="performance-panel" hidden><summary>Performance details</summary><div id="performance-detail"></div><button id="copy-performance" type="button">Copy local timing report</button><span id="copy-performance-status" role="status"></span></details><div class="workspace"><section class="results-pane" aria-label="Search results"><p id="result-summary" class="eyebrow">READY WHEN YOU ARE</p><ul id="results" aria-label="Matching files"></ul><div id="empty"> ${ icon ( "search" ) } <h2>Find what matters. Locally.</h2><p>Index a folder, then search a filename, phrase, or what you remember.</p></div></section><aside id="preview" aria-label="Result preview" hidden><div class="preview-header"><span id="preview-icon" class="file-icon"></span><div class="preview-heading"><h2 id="preview-title"></h2><p id="preview-path" class="path"></p></div></div><div class="preview-toolbar"><span id="preview-kind"></span><div class="preview-actions"><button id="open-result" type="button"> ${ icon ( "open" ) } <span>Open file</span></button><button id="reveal-result" type="button" title="Show in folder" aria-label="Show in folder"> ${ icon ( "folder" ) } </button></div></div><div id="media-preview" class="media-preview" hidden></div><p id="media-preview-status" class="hint" role="status" hidden></p><div class="passage"><p id="passage-label" class="eyebrow">EXTRACTED PASSAGE</p><pre id="preview-text"></pre></div></aside></div>
2026-10-12 17:44:56 +02:00
</main><footer><span class="footer-privacy"> ${ icon ( "lock" ) } <span>Private search. Everything stays on your device.</span><span id="privacy" class="privacy">Local</span></span><span class="keyboard-hints"><kbd> ${ icon ( "arrowUp" ) } </kbd><kbd> ${ icon ( "arrowDown" ) } </kbd><span>Navigate</span><kbd>Enter</kbd><span>Open</span><kbd>Esc</kbd><span>Clear</span></span></footer>` ;
2026-10-12 16:07:53 +02:00
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" );
2026-10-12 19:16:23 +02:00
try {
element < HTMLInputElement >( "performance-toggle" ). checked =
localStorage . getItem ( "searchi-performance-details" ) === "true" ;
} catch {
/* Diagnostics remain opt-in when storage is unavailable. */
}
2026-10-12 16:07:53 +02:00
let results : SearchResult [] = [];
let selected = - 1 ;
let requestId = 0 ;
let timer : ReturnType < typeof setTimeout >;
2026-10-12 17:44:56 +02:00
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 ;
2026-10-12 19:16:23 +02:00
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 = convertFileSrc ( 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 = convertFileSrc ( 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.` ;
},
);
2026-10-12 17:44:56 +02:00
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 Boolean (
semanticStatus ? . ready &&
semanticStatus . generation !== null &&
! semanticStatus . embedding &&
! semanticJobActive ,
);
}
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 =
2026-10-12 19:16:23 +02:00
`Semantic search indexed ${ report . chunks . toLocaleString () } content segments.` ;
2026-10-12 17:44:56 +02:00
} 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 )
2026-10-12 19:16:23 +02:00
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` : "" } ` ;
2026-10-12 17:44:56 +02:00
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." ;
2026-10-12 19:16:23 +02:00
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.` ;
}
2026-10-12 17:44:56 +02:00
const retry = element < HTMLButtonElement >( "retry-model" );
retry . hidden = ! error || status . ready ;
retry . disabled = working ;
const rebuild = element < HTMLButtonElement >( "rebuild-semantic" );
rebuild . hidden = ! status . ready || ( ! status . chunks && ! status . pending );
rebuild . disabled = working || indexingActive ;
element < HTMLSelectElement >( "cpu-threads" ). disabled =
working || indexingActive ;
if (
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
)
2026-10-12 19:16:23 +02:00
submitSearch ( latestSearch , "hybrid" );
2026-10-12 17:44:56 +02:00
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 =
status . indexing ||
semanticJobActive ||
Boolean (
semanticStatus ? . downloading ||
semanticStatus ? . warming ||
semanticStatus ? . embedding ,
);
element < HTMLButtonElement >( "add-folder" ). disabled = status . indexing ;
const roots = element ( "roots" );
roots . replaceChildren ();
for ( const root of status . settings . roots ) {
const item = document . createElement ( "li" );
const path = document . createElement ( "span" );
path . textContent = root ;
path . className = "path" ;
const reindex = document . createElement ( "button" );
reindex . type = "button" ;
reindex . textContent = "Reindex" ;
reindex . disabled = status . indexing ;
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 ;
}
2026-10-12 16:07:53 +02:00
function showError ( error : unknown ) {
notice . textContent = String ( error );
notice . className = "error" ;
}
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 );
2026-10-12 17:44:56 +02:00
button . setAttribute ( "aria-current" , String ( i === selected ));
button . tabIndex = i === selected ? 0 : - 1 ;
2026-10-12 16:07:53 +02:00
if ( i === selected && focus ) button . focus ();
});
element ( "preview" ). hidden = false ;
2026-10-12 17:44:56 +02:00
const selectedIcon = fileIcon ( result . filename );
element ( "preview-icon" ). innerHTML = selectedIcon . html ;
element ( "preview-icon" ). dataset . type = selectedIcon . type ;
2026-10-12 16:07:53 +02:00
element ( "preview-title" ). textContent = result . filename ;
element ( "preview-path" ). textContent = result . path ;
element ( "preview-kind" ). textContent =
2026-10-12 19:16:23 +02:00
` ${ matchLabel ( result . match_kind ) }${ result . source ? ` · ${ sourceLabel ( result . source ) } ` : "" } ` ;
const media = mediaKind ( result . filename );
element ( "passage-label" ). textContent = media
? "MATCHED CONTENT"
: "EXTRACTED PASSAGE" ;
2026-10-12 16:07:53 +02:00
element ( "preview-text" ). textContent =
result . snippet ||
( result . extraction_error
? `Content could not be extracted: ${ result . extraction_error } `
2026-10-12 19:16:23 +02:00
: media
? result . source ? . matched_query || "Preview the matched media below."
: "No extracted text is available for this file." );
2026-10-12 17:44:56 +02:00
element < HTMLButtonElement >( "open-result" ). title = result . source
2026-10-12 19:16:23 +02:00
? `Open in your default application. The matched location ${ sourceLabel ( result . source ) } is shown in Searchi; default application opening starts at its own location.`
2026-10-12 17:44:56 +02:00
: "Open in your default application" ;
2026-10-12 19:16:23 +02:00
const selection = previewRevision . select (
JSON . stringify ([ result . path , result . source ? . kind , result . source ? . number ]),
);
if ( selection . changed ) {
mediaLane . clear ();
unloadMedia ();
if ( media && isTauri ()) {
element ( "media-preview-status" ). hidden = false ;
element ( "media-preview-status" ). textContent = "Preparing media preview…" ;
mediaLane . submit ({ revision : selection.revision , result });
}
}
2026-10-12 17:44:56 +02:00
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 ;
}
2026-10-12 16:07:53 +02:00
}
function render() {
2026-10-12 17:44:56 +02:00
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 );
}
}
2026-10-12 16:07:53 +02:00
element ( "empty" ). hidden = results . length > 0 ;
2026-10-12 17:44:56 +02:00
if ( ! results . length ) {
element ( "preview" ). hidden = true ;
previewPath = null ;
2026-10-12 19:16:23 +02:00
clearMedia ();
2026-10-12 17:44:56 +02:00
}
2026-10-12 16:07:53 +02:00
results . forEach (( result , i ) => {
2026-10-12 17:44:56 +02:00
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 = result . path ;
entry . title . textContent = result . filename ;
entry . path . textContent = result . path ;
entry . snippet . textContent = result . snippet ;
2026-10-12 19:16:23 +02:00
entry . kind . textContent = ` ${ matchLabel ( result . match_kind ) }${ result . source ? ` · ${ sourceLabel ( result . source ) } ` : "" } ` ;
2026-10-12 17:44:56 +02:00
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 );
2026-10-12 16:07:53 +02:00
});
2026-10-12 17:44:56 +02:00
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 ;
};
2026-10-12 19:16:23 +02:00
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 ;
};
2026-10-12 17:44:56 +02:00
// 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 {
folder : input ( "filter-folder" ) || null ,
extension : input ( "filter-extension" ) || null ,
2026-10-12 19:16:23 +02:00
min_duration_ms : duration ( "filter-duration-min" ),
max_duration_ms : duration ( "filter-duration-max" ),
2026-10-12 17:44:56 +02:00
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 ;
2026-10-12 19:16:23 +02:00
const renderStarted = performance . now ();
2026-10-12 17:44:56 +02:00
const selectedPath = results [ selected ] ? . path ;
const resultsHadFocus = list . contains ( document . activeElement );
shownLane = lane ;
displayedRequestId = request . id ;
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 ();
2026-10-12 19:16:23 +02:00
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" } · NATIVE ${ briefMs ( metric ? . total_ms ?? 0 ) } MS · ${ lane === "hybrid" ? "FINAL" : "FIRST RESPONSE" } ${ Math . round ( lane === "hybrid" ? ( trace . hybrid_final_results_ms ?? 0 ) : ( trace . first_results_ms ?? 0 )) } MS` ;
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 350 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 );
2026-10-12 17:44:56 +02:00
}
const isCurrent = ( request : SearchRequest ) => request . id === requestId ;
const lexicalLane = new LatestLane < SearchRequest >(
async ( request ) => {
2026-10-12 19:16:23 +02:00
const found = await invokeMeasured ( request , "lexical" );
2026-10-12 17:44:56 +02:00
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 ;
2026-10-12 19:16:23 +02:00
const found = await invokeMeasured ( request , "hybrid" );
2026-10-12 17:44:56 +02:00
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 ;
2026-10-12 16:07:53 +02:00
}
2026-10-12 19:16:23 +02:00
async function search ( inputAt = performance . now ()) {
2026-10-12 17:44:56 +02:00
invalidateSearch ();
querySemanticFailed = false ;
shownLane = "lexical" ;
2026-10-12 16:07:53 +02:00
const value = query . value . trim ();
if ( ! value ) {
2026-10-12 19:16:23 +02:00
element ( "performance-panel" ). hidden = true ;
2026-10-12 16:07:53 +02:00
results = [];
2026-10-12 17:44:56 +02:00
displayedRequestId = null ;
selected = - 1 ;
2026-10-12 16:07:53 +02:00
render ();
element ( "result-summary" ). textContent = "READY WHEN YOU ARE" ;
element ( "empty" ). querySelector ( "h2" ) ! . textContent =
2026-10-12 17:44:56 +02:00
"Find what matters. Locally." ;
2026-10-12 16:07:53 +02:00
element ( "empty" ). querySelector ( "p" ) ! . textContent =
2026-10-12 17:44:56 +02:00
"Index a folder, then search a filename, phrase, or what you remember." ;
2026-10-12 16:07:53 +02:00
return ;
}
try {
2026-10-12 17:44:56 +02:00
const request : SearchRequest = {
id : requestId ,
2026-10-12 16:07:53 +02:00
query : value ,
2026-10-12 17:44:56 +02:00
filters : captureFilters (),
started : performance.now (),
2026-10-12 19:16:23 +02:00
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 ,
},
),
2026-10-12 17:44:56 +02:00
};
latestSearch = request ;
2026-10-12 19:16:23 +02:00
refreshPerformance ( request );
2026-10-12 17:44:56 +02:00
element ( "result-summary" ). textContent = "SEARCHING…" ;
2026-10-12 19:16:23 +02:00
submitSearch ( request , "lexical" );
2026-10-12 17:44:56 +02:00
hybridTimer = setTimeout (() => {
if ( isCurrent ( request ) && canSearchSemantically ())
2026-10-12 19:16:23 +02:00
submitSearch ( request , "hybrid" );
2026-10-12 17:44:56 +02:00
}, 350 );
2026-10-12 16:07:53 +02:00
} catch ( error ) {
2026-10-12 17:44:56 +02:00
showError ( error );
2026-10-12 16:07:53 +02:00
}
}
async function refreshStats() {
const stats = await invoke < IndexStats >( "stats" );
element ( "index-status" ). textContent =
2026-10-12 19:16:23 +02:00
` ${ stats . files . toLocaleString () } files · ${ stats . content_files . toLocaleString () } with extracted text` ;
2026-10-12 16:07:53 +02:00
}
query . addEventListener ( "input" , () => {
2026-10-12 19:16:23 +02:00
const inputAt = performance . now ();
2026-10-12 17:44:56 +02:00
invalidateSearch ();
2026-10-12 19:16:23 +02:00
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." ;
},
);
2026-10-12 16:07:53 +02:00
});
element ( "search-form" ). addEventListener ( "submit" , ( event ) => {
event . preventDefault ();
clearTimeout ( timer );
void search ();
});
2026-10-12 17:44:56 +02:00
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 ;
});
});
2026-10-12 16:07:53 +02:00
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" , () => {
void search ();
});
2026-10-12 17:44:56 +02:00
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 ();
});
2026-10-12 16:07:53 +02:00
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 );
});
2026-10-12 17:44:56 +02:00
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 );
}
});
2026-10-12 16:07:53 +02:00
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 ;
2026-10-12 17:44:56 +02:00
element < HTMLSelectElement >( "cpu-threads" ). disabled = true ;
indexingActive = true ;
element ( "index-status" ). textContent =
"Indexing active · Search remains available" ;
2026-10-12 16:07:53 +02:00
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 ;
2026-10-12 17:44:56 +02:00
folderRevision ++ ;
localSemanticError = null ;
2026-10-12 16:07:53 +02:00
await refreshStats ();
await search ();
} catch ( error ) {
showError ( error );
} finally {
buttons . forEach (( button ) => {
button . disabled = false ;
});
element < HTMLButtonElement >( "add-folder" ). disabled = false ;
2026-10-12 17:44:56 +02:00
await refreshDesktopStatus (). catch ( showError );
await refreshStats (). catch ( showError );
queueSemanticRefresh ( 0 );
2026-10-12 16:07:53 +02:00
}
});
document . addEventListener ( "keydown" , ( event ) => {
if (( event . ctrlKey || event . metaKey ) && event . key . toLowerCase () === "k" ) {
event . preventDefault ();
query . focus ();
query . select ();
}
if ( event . key === "Escape" ) {
2026-10-12 19:16:23 +02:00
clearMedia ();
2026-10-12 17:44:56 +02:00
document
. querySelectorAll < HTMLDetailsElement >( ".popover[open]" )
. forEach (( popover ) => {
popover . open = false ;
});
2026-10-12 16:07:53 +02:00
++ requestId ;
query . value = "" ;
void search ();
query . focus ();
}
if ( event . target === query || list . contains ( event . target as Node )) {
2026-10-12 17:44:56 +02:00
if (
event . key === "Enter" &&
results [ selected ] &&
displayedRequestId === requestId
) {
event . preventDefault ();
void openSelected ( false );
}
2026-10-12 16:07:53 +02:00
if ( event . key === "ArrowDown" || event . key === "ArrowUp" ) {
event . preventDefault ();
select ( selected + ( event . key === "ArrowDown" ? 1 : - 1 ), true );
}
}
});
if ( isTauri ()) {
void refreshStats (). catch ( showError );
2026-10-12 17:44:56 +02:00
void refreshDesktopStatus (). catch ( showError );
void refreshSemanticStatus ();
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 );
2026-10-12 16:07:53 +02:00
query . focus ();
} else {
element ( "index-status" ). textContent = "Desktop preview" ;
2026-10-12 17:44:56 +02:00
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." ;
2026-10-12 16:07:53 +02:00
showError ( "Run the Tauri desktop app to index and search real files." );
}