Enhance semantic indexing and UI status display

- Updated the `semanticIndex` module to support GPU acceleration with ONNX Runtime, improving performance on compatible devices.
- Introduced new functions to determine preferred embedding devices and handle device status, enhancing the flexibility of the embedding process.
- Expanded the `IndexStatus` interface to include backend type, device information, and runtime details for better tracking of indexing status.
- Enhanced the UI in the settings panel to display runtime and device information, providing users with clearer insights into the indexing process and model usage.
This commit is contained in:
Pawan Osman
2026-07-15 15:37:43 +03:00
parent a3d917d1b5
commit d47e978be7
3 changed files with 255 additions and 7 deletions
+109 -4
View File
@@ -8,8 +8,9 @@
*/
// Real local semantic codebase index.
// - Embeddings: @huggingface/transformers (Xenova/all-MiniLM-L6-v2, q8 ONNX/WASM)
// → 100% local, no server, no API key. Model downloads once to globalStorage.
// - Embeddings: @huggingface/transformers + onnxruntime-node
// (Xenova/all-MiniLM-L6-v2, q8). GPU when available (DML/CUDA/CoreML/WebGPU),
// else CPU. 100% local; model downloads once to globalStorage.
// - Chunking: sliding line-window per file (simple, language-agnostic).
// ponytail: line-window chunking; upgrade to tree-sitter AST chunks when
// ranking quality on large funcs matters.
@@ -59,6 +60,7 @@ export function setEmbedModel(id: string): void {
if (m.id !== activeModel.id) {
activeModel = m;
extractorP = null; // force reload with new repo/dtype
embedDevice = null;
}
if (changed) {
memIndex = null; // index built with old model is stale
@@ -136,7 +138,33 @@ export function setIndexStorageDir(dir: string): void {
}
// ---- Embedder (lazy, singleton) ----
// ONNX Runtime Node EPs: Win→dml, Linux x64→cuda, macOS→coreml, then webgpu, then cpu.
// transformers.js defaults to CPU only; we try GPU when available and fall back.
function preferredEmbedDevices(): string[] {
const order: string[] = [];
switch (process.platform) {
case "win32":
order.push("dml"); // DirectML (any DX12 GPU)
break;
case "linux":
if (process.arch === "x64") order.push("cuda"); // needs CUDA 12 + cuDNN
break;
case "darwin":
order.push("coreml");
break;
}
order.push("webgpu", "cpu");
return order;
}
let extractorP: Promise<any> | null = null;
let embedDevice: string | null = null;
/** Device the live embedder is using (null until first load). */
export function getEmbedDevice(): string | null {
return embedDevice;
}
async function getExtractor(): Promise<any | null> {
if (!storageDir) return null;
if (!extractorP) {
@@ -145,10 +173,31 @@ async function getExtractor(): Promise<any | null> {
const t = await importRuntimeDep("@huggingface/transformers");
t.env.allowRemoteModels = true;
t.env.cacheDir = path.join(storageDir!, "models");
return t.pipeline("feature-extraction", m.repo, { dtype: m.dtype });
// Try GPU EPs first; ORT often fails hard if a provider's libs are missing,
// so probe one device at a time instead of device:"auto".
const devices = preferredEmbedDevices();
let lastErr: unknown;
for (const device of devices) {
try {
const pipe = await t.pipeline("feature-extraction", m.repo, {
dtype: m.dtype,
device,
});
embedDevice = device;
console.log(`[semanticIndex] embedder on ${device} (${m.id})`);
// UI may already be open; push device once the pipeline is ready.
if (memRoot) emitStatus(memRoot);
return pipe;
} catch (e) {
lastErr = e;
console.warn(`[semanticIndex] embedder device "${device}" unavailable, trying next…`);
}
}
throw lastErr ?? new Error("no embedder device available");
})().catch((e) => {
console.error("[semanticIndex] embedder load failed:", e);
extractorP = null;
embedDevice = null;
return null;
});
}
@@ -282,7 +331,24 @@ export interface IndexStatus {
total: number;
files: number; // indexed files in store
chunks: number;
model: string; // active EmbedModel.id
model: string; // active EmbedModel.id or remote model id
/** "local" = onnxruntime-node; "remote" = provider /embeddings API. */
backend: "local" | "remote";
/** ONNX EP in use: dml | cuda | coreml | webgpu | cpu. Null until first load / remote. */
device: string | null;
/** GPU-class EP vs CPU (remote counts as neither — uses provider). */
accelerator: "gpu" | "cpu" | "remote" | "pending";
/** Human-readable device label for the UI. */
deviceLabel: string;
/** Active local model technical fields (undefined when remote). */
modelRepo?: string;
modelDtype?: string;
modelPooling?: string;
modelDim?: number;
/** Remote endpoint host when using a provider embedding model. */
remoteBaseUrl?: string;
runtime: string;
platform: string;
}
let progress = { done: 0, total: 0 };
const statusSubs = new Set<(s: IndexStatus) => void>();
@@ -290,8 +356,36 @@ export function onIndexStatus(fn: (s: IndexStatus) => void): () => void {
statusSubs.add(fn);
return () => statusSubs.delete(fn);
}
function deviceLabelOf(device: string | null, backend: "local" | "remote"): string {
if (backend === "remote") return "Remote API";
if (!device) return "Not loaded yet";
switch (device) {
case "dml":
return "GPU · DirectML";
case "cuda":
return "GPU · CUDA";
case "coreml":
return "GPU · CoreML";
case "webgpu":
return "GPU · WebGPU";
case "cpu":
return "CPU";
default:
return device;
}
}
function acceleratorOf(device: string | null, backend: "local" | "remote"): IndexStatus["accelerator"] {
if (backend === "remote") return "remote";
if (!device) return "pending";
return device === "cpu" ? "cpu" : "gpu";
}
export function getStatus(root: string): IndexStatus {
const idx = memRoot === normRoot(root) ? memIndex : null;
const backend: "local" | "remote" = remoteCfg ? "remote" : "local";
const device = backend === "remote" ? null : embedDevice;
return {
indexing,
done: progress.done,
@@ -299,6 +393,17 @@ export function getStatus(root: string): IndexStatus {
files: idx ? Object.keys(idx.files).length : 0,
chunks: idx ? idx.chunks.length : 0,
model: getEmbedModelId(),
backend,
device,
accelerator: acceleratorOf(device, backend),
deviceLabel: deviceLabelOf(device, backend),
modelRepo: backend === "local" ? activeModel.repo : undefined,
modelDtype: backend === "local" ? activeModel.dtype : undefined,
modelPooling: backend === "local" ? activeModel.pooling : undefined,
modelDim: backend === "local" ? activeModel.dim : undefined,
remoteBaseUrl: remoteCfg?.baseUrl,
runtime: backend === "local" ? "onnxruntime-node + @huggingface/transformers" : "OpenAI-compatible /embeddings",
platform: `${process.platform}-${process.arch}`,
};
}
function emitStatus(root: string): void {
+100 -3
View File
@@ -41,6 +41,17 @@ interface IndexStatus {
files: number;
chunks: number;
model: string;
backend?: "local" | "remote";
device?: string | null;
accelerator?: "gpu" | "cpu" | "remote" | "pending";
deviceLabel?: string;
modelRepo?: string;
modelDtype?: string;
modelPooling?: string;
modelDim?: number;
remoteBaseUrl?: string;
runtime?: string;
platform?: string;
}
interface EmbedModel {
id: string;
@@ -535,6 +546,11 @@ function IndexingPanel({
}) {
const pct = status.total > 0 ? Math.round((status.done / status.total) * 100) : status.files > 0 ? 100 : 0;
const remoteEmbed = modelList.filter((m) => isEmbeddingModel(m.id));
const accel = status.accelerator || "pending";
const accelClass =
accel === "gpu" ? "gpu" : accel === "cpu" ? "cpu" : accel === "remote" ? "remote" : "pending";
const accelText =
accel === "gpu" ? "GPU" : accel === "cpu" ? "CPU" : accel === "remote" ? "Remote" : "Loading...";
return (
<>
<h1 className="page-title">Indexing &amp; Docs</h1>
@@ -543,7 +559,7 @@ function IndexingPanel({
<div className="index-card-title">Codebase Indexing</div>
<p className="row-desc">
Embed codebase for improved contextual understanding and knowledge. Embeddings and metadata are
stored locally on your machine your code never leaves your computer. The index persists across
stored locally on your machine - your code never leaves your computer. The index persists across
restarts; only new or changed files are re-embedded.
</p>
<div className="index-divider" />
@@ -563,17 +579,98 @@ function IndexingPanel({
? `Disabled - ${status.files} files on disk`
: status.indexing
? `Indexing ${status.done} / ${status.total} files...`
: `${status.files} files`}
: `${status.files} files / ${status.chunks || 0} chunks`}
</div>
</div>
<div className="index-divider" />
<div className="index-runtime">
<div className="index-runtime-head">
<span className={`index-accel-badge ${accelClass}`}>{accelText}</span>
<span className="index-runtime-device">{status.deviceLabel || "-"}</span>
</div>
<dl className="index-tech">
<div>
<dt>Model</dt>
<dd>
<code>{status.model}</code>
</dd>
</div>
{status.modelRepo && (
<div>
<dt>Repo</dt>
<dd>
<code>{status.modelRepo}</code>
</dd>
</div>
)}
{status.modelDtype && (
<div>
<dt>Dtype</dt>
<dd>
<code>{status.modelDtype}</code>
</dd>
</div>
)}
{status.modelPooling && (
<div>
<dt>Pooling</dt>
<dd>
<code>{status.modelPooling}</code>
</dd>
</div>
)}
{status.modelDim != null && (
<div>
<dt>Dimensions</dt>
<dd>
<code>{status.modelDim}</code>
</dd>
</div>
)}
{status.device != null && status.device !== "" && (
<div>
<dt>ONNX EP</dt>
<dd>
<code>{status.device}</code>
</dd>
</div>
)}
<div>
<dt>Backend</dt>
<dd>
<code>{status.backend || "local"}</code>
</dd>
</div>
{status.remoteBaseUrl && (
<div>
<dt>Endpoint</dt>
<dd>
<code>{status.remoteBaseUrl}</code>
</dd>
</div>
)}
<div>
<dt>Runtime</dt>
<dd>
<code>{status.runtime || "—"}</code>
</dd>
</div>
<div>
<dt>Platform</dt>
<dd>
<code>{status.platform || "—"}</code>
</dd>
</div>
</dl>
</div>
<div className="index-divider" />
<div className="index-model-row">
<span className="index-model-label">Embedding model</span>
<ModelSelect
models={remoteEmbed}
value={status.model}
onChange={(id) => !status.indexing && features.indexingEnabled !== false && vscode.postMessage({ type: "setEmbedModel", modelId: id })}
customItems={models.map((m) => ({ value: m.id, label: m.name, desc: "local runs on your machine" }))}
customItems={models.map((m) => ({ value: m.id, label: m.name, desc: "local - runs on your machine" }))}
style={{ maxWidth: 260 }}
/>
<div className="index-actions" style={{ marginTop: 0, marginLeft: "auto" }}>
+46
View File
@@ -285,6 +285,52 @@ select:focus { outline: none; border-color: var(--vscode-focusBorder); }
.index-progress-meta { font-size: 11.5px; color: var(--fg-dim); margin-top: 6px; }
.index-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; }
.index-runtime { margin: 14px 0 4px; }
.index-runtime-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
.index-runtime-device { font-size: 12.5px; font-weight: 600; color: var(--fg); }
.index-accel-badge {
display: inline-flex; align-items: center; justify-content: center;
font-size: 10.5px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
padding: 2px 8px; border-radius: 999px; border: 1px solid transparent;
}
.index-accel-badge.gpu {
color: var(--green);
background: color-mix(in srgb, var(--green) 14%, transparent);
border-color: color-mix(in srgb, var(--green) 35%, transparent);
}
.index-accel-badge.cpu {
color: var(--fg-dim);
background: color-mix(in srgb, var(--fg) 8%, transparent);
border-color: color-mix(in srgb, var(--fg) 18%, transparent);
}
.index-accel-badge.remote {
color: var(--accent);
background: color-mix(in srgb, var(--accent) 14%, transparent);
border-color: color-mix(in srgb, var(--accent) 35%, transparent);
}
.index-accel-badge.pending {
color: var(--fg-faint);
background: color-mix(in srgb, var(--fg) 6%, transparent);
border-color: color-mix(in srgb, var(--fg) 14%, transparent);
}
.index-tech {
display: grid; grid-template-columns: 1fr 1fr; gap: 6px 16px; margin: 0;
padding: 10px 12px; border-radius: 8px;
background: color-mix(in srgb, var(--fg) 4%, transparent);
border: 1px solid var(--border-soft);
}
.index-tech > div { display: grid; grid-template-columns: 88px 1fr; gap: 6px; align-items: baseline; min-width: 0; }
.index-tech dt { margin: 0; font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--fg-dim); }
.index-tech dd { margin: 0; min-width: 0; font-size: 11.5px; color: var(--fg); }
.index-tech code {
font-family: var(--vscode-editor-font-family, ui-monospace, monospace);
font-size: 11px; word-break: break-all;
color: var(--fg);
}
@media (max-width: 720px) {
.index-tech { grid-template-columns: 1fr; }
}
/* Indexing & Docs page (Cursor layout) */
.index-divider { height: 1px; background: var(--border-soft); margin: 14px -16px; }
.index-model-row { display: flex; align-items: center; gap: 10px; }