feat: add Google code retrieval across desktop CLI and MCP
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Canceled after 2m23s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Canceled after 2m23s
This commit is contained in:
1 parent
8b9b079469
commit
81846b3e98
32 files changed
+1303
-59
No files matched your search
@@ -12,9 +12,10 @@ import {
|
||||
emptySearchState,
|
||||
LatestLane,
|
||||
semanticAvailable,
|
||||
scopedFilters,
|
||||
SEMANTIC_SETTLE_MS,
|
||||
} from "./search-state";
|
||||
import type { SemanticPhase } from "./search-state";
|
||||
import type { SemanticPhase, SearchFilters } from "./search-state";
|
||||
import { fileIcon, icon } from "./icons";
|
||||
import {
|
||||
SearchMeasurement,
|
||||
@@ -108,13 +109,13 @@ interface SemanticStatus {
|
||||
interface SearchRequest {
|
||||
id: number;
|
||||
query: string;
|
||||
filters: Record<string, string | number | null>;
|
||||
filters: SearchFilters;
|
||||
started: number;
|
||||
measurement: SearchMeasurement;
|
||||
}
|
||||
|
||||
document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
|
||||
<header><div class="brand"><img class="brand-symbol" src="/searchi-symbol.png" alt=""><span class="wordmark">searchi</span></div><span id="index-status" class="index-status">Loading your index…</span><div class="header-actions"><button id="add-folder" class="quiet-button" type="button">${icon("folderAdd")}<span>Add folder</span></button><details id="filters" class="popover"><summary>${icon("filter")}<span>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><details id="ai-settings" class="ai-settings"><summary>Optional AI answers</summary><label class="performance-toggle"><input id="ai-enabled" type="checkbox">Enable summaries and questions</label><p class="hint">Disabled by default. Only an explicit action sends the shown passage to your configured endpoint.</p><label>OpenAI-compatible base URL<input id="ai-endpoint" type="url" placeholder="http://localhost:1234/v1" autocomplete="off" maxlength="2048"></label><label>Model<input id="ai-model" placeholder="Your model name" autocomplete="off" maxlength="200"></label><label>API key (optional, this session only)<input id="ai-key" type="password" autocomplete="off" maxlength="2048"></label><button id="ai-clear-key" type="button">Clear key</button><p class="hint">Endpoint, model and key stay in this app session. The key authenticates requests to your endpoint.</p></details></div></details></div></header>
|
||||
<header><div class="brand"><img class="brand-symbol" src="/searchi-symbol.png" alt=""><span class="wordmark">searchi</span></div><span id="index-status" class="index-status">Loading your index…</span><div class="header-actions"><button id="add-folder" class="quiet-button" type="button">${icon("folderAdd")}<span>Add folder</span></button><details id="filters" class="popover"><summary>${icon("filter")}<span id="scope-label">All files</span>${icon("chevron")}</summary><div class="popover-panel"><h2>Filter your search</h2><div class="filters-grid"><label>Search scope<select id="filter-scope"><option value="all">All files</option><option value="code">Code</option></select></label><label>Within folder<input id="filter-folder" placeholder="Absolute folder path"></label><label>Extension<input id="filter-extension" placeholder="pdf, md, txt…"></label><label>Minimum size (bytes)<input id="filter-min" type="number" min="0" step="1"></label><label>Maximum size (bytes)<input id="filter-max" type="number" min="0" step="1"></label><label>Minimum duration (seconds)<input id="filter-duration-min" type="number" min="0" step="0.1"></label><label>Maximum duration (seconds)<input id="filter-duration-max" type="number" min="0" step="0.1"></label><label>Modified from<input id="filter-after" type="date"></label><label>Modified through<input id="filter-before" type="date"></label></div></div></details><details id="settings" class="popover"><summary class="icon-button" aria-label="Settings" title="Settings">${icon("settings")}</summary><div class="popover-panel"><h2>Folders & resources</h2><p id="shortcut-status" class="hint"></p><label class="resource-label" for="cpu-threads">CPU thread budget</label><select id="cpu-threads">${Array.from({ length: 8 }, (_, i) => `<option value="${i + 1}">${i + 1} ${i ? "threads" : "thread (default)"}</option>`).join("")}</select><p class="hint">Search automatically uses supported acceleration with CPU fallback.</p><ul id="roots" aria-label="Selected folders"></ul><label class="performance-toggle"><input id="performance-toggle" type="checkbox">Performance details</label><p class="hint">Local timing diagnostics for new searches. Queries and file paths are excluded from copied reports.</p><details id="ai-settings" class="ai-settings"><summary>Optional AI answers</summary><label class="performance-toggle"><input id="ai-enabled" type="checkbox">Enable summaries and questions</label><p class="hint">Disabled by default. Only an explicit action sends the shown passage to your configured endpoint.</p><label>OpenAI-compatible base URL<input id="ai-endpoint" type="url" placeholder="http://localhost:1234/v1" autocomplete="off" maxlength="2048"></label><label>Model<input id="ai-model" placeholder="Your model name" autocomplete="off" maxlength="200"></label><label>API key (optional, this session only)<input id="ai-key" type="password" autocomplete="off" maxlength="2048"></label><button id="ai-clear-key" type="button">Clear key</button><p class="hint">Endpoint, model and key stay in this app session. The key authenticates requests to your endpoint.</p></details></div></details></div></header>
|
||||
<main>
|
||||
<form id="search-form" role="search"><label class="sr-only" for="query">Search your files</label>${icon("search")}<input id="query" type="search" placeholder="Search a filename, phrase, or what you remember…" autocomplete="off" spellcheck="false" maxlength="1000"><kbd id="focus-shortcut">Ctrl K</kbd><button id="clear-query" class="icon-button" type="button" aria-label="Clear search" title="Clear search">${icon("dismiss")}</button></form>
|
||||
<details id="semantic-panel"><summary>${icon("sparkles")}<span id="semantic-status" role="status" aria-live="polite">Preparing local semantic search…</span><span id="semantic-device" class="kind">Local model</span>${icon("chevron")}</summary><div class="semantic-content"><strong>Google EmbeddingGemma 2</strong><progress id="semantic-progress" aria-label="Local model setup progress"></progress><p id="semantic-detail" class="hint">The verified local model downloads automatically on first launch (865 MB). Your files stay on this device.</p><div class="semantic-actions"><button id="retry-model" type="button" hidden>Retry model setup</button><button id="rebuild-semantic" type="button" hidden>Rebuild semantic index</button></div></div></details>
|
||||
@@ -135,6 +136,7 @@ try {
|
||||
}
|
||||
let results: SearchResult[] = [];
|
||||
let displayedLiteralQuery = literalQuery("");
|
||||
let displayedCodeScope = false;
|
||||
let explainedSelection = "";
|
||||
let selected = -1;
|
||||
let requestId = 0;
|
||||
@@ -726,6 +728,7 @@ function select(index: number, focus = false) {
|
||||
: 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,
|
||||
@@ -833,6 +836,7 @@ function render() {
|
||||
"semantic_video",
|
||||
"semantic_audio",
|
||||
].includes(result.match_kind);
|
||||
entry.snippet.classList.toggle("code-snippet", displayedCodeScope);
|
||||
renderHighlighted(
|
||||
entry.snippet,
|
||||
displayExcerpt(result.snippet, displayedLiteralQuery),
|
||||
@@ -879,7 +883,7 @@ function captureFilters() {
|
||||
throw new Error("Enter a valid modification date.");
|
||||
return stamp;
|
||||
};
|
||||
return {
|
||||
return scopedFilters(element<HTMLSelectElement>("filter-scope").value, {
|
||||
folder: input("filter-folder") || null,
|
||||
extension: input("filter-extension") || null,
|
||||
min_duration_ms: duration("filter-duration-min"),
|
||||
@@ -888,7 +892,7 @@ function captureFilters() {
|
||||
max_size: size("filter-max"),
|
||||
modified_after_unix_ms: date("filter-after", false),
|
||||
modified_before_unix_ms: date("filter-before", true),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function applyResults(
|
||||
@@ -907,6 +911,7 @@ function applyResults(
|
||||
shownLane = lane;
|
||||
displayedRequestId = request.id;
|
||||
displayedLiteralQuery = literalQuery(request.query);
|
||||
displayedCodeScope = request.filters.code_only === true;
|
||||
results = found;
|
||||
selected = -1;
|
||||
render();
|
||||
@@ -1209,6 +1214,10 @@ element("browse-folder").addEventListener("click", async () => {
|
||||
}
|
||||
});
|
||||
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 () => {
|
||||
|
||||
@@ -195,7 +195,9 @@ export function matchExplanation(result: MatchResult) {
|
||||
? ` on page ${source.number}`
|
||||
: source?.kind === "paragraph"
|
||||
? ` in paragraph ${source.number}`
|
||||
: "";
|
||||
: source?.kind === "line"
|
||||
? ` at line ${source.number}`
|
||||
: "";
|
||||
const time =
|
||||
source?.kind === "timestamp_ms"
|
||||
? ` near ${timestampLabel(source.number)}`
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/** Scope is captured with filters; changing controls cannot mutate active work. */
|
||||
export type SearchFilters = Readonly<
|
||||
Record<string, string | number | boolean | null>
|
||||
>;
|
||||
export function scopedFilters(
|
||||
scope: string,
|
||||
filters: Record<string, string | number | boolean | null>,
|
||||
): SearchFilters {
|
||||
if (scope !== "all" && scope !== "code")
|
||||
throw new Error("Choose a valid search scope.");
|
||||
return Object.freeze({ ...filters, code_only: scope === "code" });
|
||||
}
|
||||
|
||||
/** Let rapid typing settle before starting the model lane. */
|
||||
export const SEMANTIC_SETTLE_MS = 120;
|
||||
|
||||
|
||||
@@ -937,3 +937,15 @@ mark[data-match="literal"] {
|
||||
border-color: Highlight;
|
||||
}
|
||||
}
|
||||
|
||||
/* Preserve actual source indentation without introducing generated annotations. */
|
||||
.snippet.code-snippet,
|
||||
aside pre.code-passage {
|
||||
font-family:
|
||||
ui-monospace, "Cascadia Code", "SFMono-Regular", Consolas, monospace;
|
||||
white-space: pre-wrap;
|
||||
tab-size: 4;
|
||||
}
|
||||
aside pre.code-passage {
|
||||
line-height: 1.6;
|
||||
}
|
||||
Reference in new issue
Block a user