mirror of
https://github.com/PawanOsman/ChatGPT.git
synced 2026-07-18 08:05:57 +02:00
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:
+109
-4
@@ -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
@@ -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 & 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" }}>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user