diff --git a/CHANGELOG.md b/CHANGELOG.md index 8294c4c..8afaa47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to the "ocursor" extension will be documented in this file. Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## [0.0.2] - 2026-07-05 + +### Added + +- Per-workspace conversations (existing global conversations migrate automatically) +- GGUF models auto-load on first message with a "loading model" card in chat +- llama.cpp server uses random free ports with retry on bind failure + +### Changed + +- Composer dropdowns (model picker, mode menu) now position themselves within the viewport and work in edit mode +- All composers share one selected model and mode +- Auto model selection hidden for now; first enabled model is the default + +### Fixed + +- Production error: `Cannot find package '@huggingface/hub'` (runtime deps now resolved via file URLs) + +### Removed + +- MCP tool marketplace + ## [0.0.1] - 2026-07-05 ### Added diff --git a/package.json b/package.json index ec82fc6..6c88af5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "ocursor", "displayName": "OpenCursor Agent", "description": "AI coding agent chat inside VS Code", - "version": "0.0.1", + "version": "0.0.2", "publisher": "pkrd", "license": "MIT", "icon": "media/icon.png", diff --git a/src/agent/llamacpp.ts b/src/agent/llamacpp.ts index 08595ed..c45e73b 100644 --- a/src/agent/llamacpp.ts +++ b/src/agent/llamacpp.ts @@ -20,9 +20,10 @@ import * as fs from "fs/promises"; import { createWriteStream } from "fs"; import * as path from "path"; import * as os from "os"; +import * as net from "net"; import { spawn, execFile } from "child_process"; import * as vscode from "vscode"; -import { ensureRuntimeDeps } from "../runtimeDeps"; +import { importRuntimeDep } from "../runtimeDeps"; /** * llama-server launch configuration. Used both as the global default and as a @@ -132,7 +133,18 @@ const LEGACY_BIN = "llama-server"; /** Resolved at install-check time: argv prefix to launch the server. */ let serverCmd: { bin: string; pre: string[] } = { bin: UNIFIED_BIN, pre: ["serve"] }; const MAX_LOG_LINES = 500; -let BASE_PORT = 8080; + +/** Ask the OS for a free ephemeral port (bind :0, read the assigned port). */ +function getFreePort(host = "127.0.0.1"): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.once("error", reject); + srv.listen(0, host, () => { + const port = (srv.address() as net.AddressInfo).port; + srv.close(() => resolve(port)); + }); + }); +} let modelsDir: string | undefined; let extCtx: vscode.ExtensionContext | undefined; @@ -225,8 +237,7 @@ export async function installLlamacpp(): Promise { // ---- HF GGUF search ---- export async function searchGguf(query: string, limit = 20): Promise { - if (!(await ensureRuntimeDeps())) throw new Error("runtime deps unavailable"); - const hub = await import("@huggingface/hub"); + const hub = await importRuntimeDep("@huggingface/hub"); const out: HfGgufResult[] = []; for await (const m of hub.listModels({ search: { query, tags: ["gguf"] }, @@ -240,8 +251,7 @@ export async function searchGguf(query: string, limit = 20): Promise { - if (!(await ensureRuntimeDeps())) throw new Error("runtime deps unavailable"); - const hub = await import("@huggingface/hub"); + const hub = await importRuntimeDep("@huggingface/hub"); const files: HfGgufResult[] = []; for await (const f of hub.listFiles({ repo, recursive: true })) { if (f.type === "file" && /\.gguf$/i.test(f.path)) { @@ -297,7 +307,6 @@ export async function importGguf(srcPath: string): Promise { return makeModel({ file: base, filePath: dest, sizeBytes: stat.size, name: path.basename(base, ".gguf") }); } -let portCursor = 0; function makeModel(p: { repo?: string; file: string; filePath: string; sizeBytes?: number; name: string }): LlamacppModel { return { id: modelId(p.repo, p.file), @@ -306,7 +315,7 @@ function makeModel(p: { repo?: string; file: string; filePath: string; sizeBytes repo: p.repo, file: p.file, sizeBytes: p.sizeBytes, - port: BASE_PORT + (portCursor++ % 100), + port: 0, // assigned per-load: a fresh random free port every time autoLoad: false, }; } @@ -347,9 +356,11 @@ export function effectiveContextLength(m: LlamacppModel, globalCtx: number): num return cfg.ctxSize ?? globalCtx; } -/** Effective bind port: per-model config override (when set) else the model's assigned port. */ +/** Effective bind port: the running server's port, else a per-model config override. */ function effectivePort(m: LlamacppModel, cfg: LlamacppServerConfig): number { - return m.useCustomConfig && cfg.port ? cfg.port : m.port; + const r = running.get(m.id); + if (r) return r.port; + return (m.useCustomConfig && cfg.port) || 0; } /** Base URL of a model's local OpenAI-compatible server (no trailing slash). */ @@ -360,8 +371,8 @@ export function serverUrlFor(m: LlamacppModel, globalCfg?: LlamacppServerConfig) } /** Build the llama-server argv from a model + effective config. */ -function buildArgs(m: LlamacppModel, cfg: LlamacppServerConfig): string[] { - const args: string[] = ["-m", m.filePath, "--port", String(effectivePort(m, cfg)), "--host", cfg.host || "127.0.0.1"]; +function buildArgs(m: LlamacppModel, cfg: LlamacppServerConfig, port: number): string[] { + const args: string[] = ["-m", m.filePath, "--port", String(port), "--host", cfg.host || "127.0.0.1"]; if (cfg.ctxSize != null) args.push("--ctx-size", String(cfg.ctxSize)); args.push(cfg.jinja === false ? "--no-jinja" : "--jinja"); if (cfg.flashAttn) args.push("-fa", cfg.flashAttn); @@ -390,19 +401,53 @@ export async function loadModel(m: LlamacppModel, globalCfg?: LlamacppServerConf errors.delete(m.id); logs.set(m.id, []); // fresh log per load loading.set(m.id, true); + emit(); // surface loading state immediately // Back-compat: callers used to pass a global context length number. const gcfg: LlamacppServerConfig | undefined = typeof globalCfg === "number" ? { ctxSize: globalCfg } : globalCfg; const cfg = effectiveConfig(m, gcfg); - const port = effectivePort(m, cfg); const host = cfg.host && cfg.host !== "0.0.0.0" ? cfg.host : "127.0.0.1"; - const argv = [...serverCmd.pre, ...buildArgs(m, cfg)]; + + // Always launch on a fresh OS-assigned random port. If the server still + // fails to bind (TOCTOU race with another process), retry with a new one. + const MAX_BIND_TRIES = 3; + let lastErr: Error | null = null; + for (let attempt = 1; attempt <= MAX_BIND_TRIES; attempt++) { + let port: number; + try { + port = await getFreePort(cfg.host || "127.0.0.1"); + } catch (e: any) { + lastErr = new Error(`could not find a free port: ${e?.message || e}`); + break; + } + try { + await spawnServer(m, cfg, host, port); + return; // loaded + } catch (e: any) { + lastErr = e instanceof Error ? e : new Error(String(e)); + // Bind failure → retry on a new random port; anything else is fatal. + const bindFail = /couldn't bind|address already in use|EADDRINUSE|HTTP server error/i.test(lastErr.message) || + (logs.get(m.id) || []).some((l) => /couldn't bind|address already in use/i.test(l)); + if (!bindFail) break; + appendLog(m.id, `[retry] port ${port} unavailable, trying a new random port (${attempt}/${MAX_BIND_TRIES})`); + } + } + loading.delete(m.id); + const msg = lastErr?.message || "failed to start server"; + errors.set(m.id, msg); + emit(); + throw new Error(msg); +} + +/** Spawn one llama-server on `port` and resolve when /health reports ready. */ +function spawnServer(m: LlamacppModel, cfg: LlamacppServerConfig, host: string, port: number): Promise { + const argv = [...serverCmd.pre, ...buildArgs(m, cfg, port)]; appendLog(m.id, `$ ${serverCmd.bin} ${argv.join(" ")}`); const proc = spawn(serverCmd.bin, argv, { stdio: ["ignore", "pipe", "pipe"] }); running.set(m.id, { proc, port }); proc.stdout?.on("data", (b) => appendLog(m.id, b.toString())); proc.stderr?.on("data", (b) => appendLog(m.id, b.toString())); - emit(); // surface loading state immediately + emit(); let resolved = false; return new Promise((resolve, reject) => { @@ -410,8 +455,6 @@ export async function loadModel(m: LlamacppModel, globalCfg?: LlamacppServerConf if (resolved) return; resolved = true; running.delete(m.id); - loading.delete(m.id); - errors.set(m.id, msg); appendLog(m.id, `[error] ${msg}`); emit(); reject(new Error(msg)); @@ -432,7 +475,7 @@ export async function loadModel(m: LlamacppModel, globalCfg?: LlamacppServerConf if (r.ok) { resolved = true; loading.delete(m.id); - appendLog(m.id, "[ready] model loaded"); + appendLog(m.id, `[ready] model loaded on port ${port}`); emit(); resolve(); return; diff --git a/src/agent/provider.ts b/src/agent/provider.ts index 35ba055..d6ed377 100644 --- a/src/agent/provider.ts +++ b/src/agent/provider.ts @@ -311,10 +311,32 @@ function parseTitle(content: string): string { } /** Auto mode judge: pick the best-suited model id from candidates for a task. */ -export async function pickModel(apiBaseUrl: string, apiKey: string, judge: string, candidates: string[], task: string, anthropic?: boolean): Promise { +export async function pickModel(apiBaseUrl: string, apiKey: string, judge: string, candidates: string[], task: string, anthropic?: boolean, oauthKind?: OAuthKind): Promise { const useAnthropic = anthropic ?? isAnthropic(apiBaseUrl); const sys = `You route a coding task to the best model. Available models: ${candidates.join(", ")}. Reply with EXACTLY one model id from the list, nothing else.`; const prompt = task.slice(0, 2000); + if (oauthKind) { + // OAuth judges (Claude Code / Codex) have no raw HTTP endpoint; stream a tiny completion. + let text = ""; + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), 30_000); + try { + const gen = streamOAuthChat(oauthKind, { + model: judge, + messages: [{ role: "system", content: sys }, { role: "user", content: prompt }], + maxTokens: 64, + signal: ctrl.signal, + }); + for await (const ev of gen) { + if (ev.type === "text-delta") text += ev.text; + } + } finally { + clearTimeout(timer); + } + // Reasoning models may emit blocks; last non-empty line is the answer. + const lines = text.replace(/[\s\S]*?<\/think>/gi, "").split("\n").map((l) => l.trim()).filter(Boolean); + return lines.length ? lines[lines.length - 1] : text.trim(); + } if (useAnthropic) { const r = await fetch(`${apiBaseUrl}/messages`, { method: "POST", diff --git a/src/agent/semanticIndex.ts b/src/agent/semanticIndex.ts index 86f20fd..38ecfe3 100644 --- a/src/agent/semanticIndex.ts +++ b/src/agent/semanticIndex.ts @@ -21,7 +21,7 @@ import * as fs from "fs/promises"; import * as path from "path"; import * as crypto from "crypto"; import { walk } from "./tools/shared"; -import { ensureRuntimeDeps } from "../runtimeDeps"; +import { importRuntimeDep } from "../runtimeDeps"; // Selectable local embedding models. Add entries here to offer more choices. export interface EmbedModel { @@ -111,8 +111,7 @@ async function getExtractor(): Promise { if (!extractorP) { const m = activeModel; extractorP = (async () => { - if (!(await ensureRuntimeDeps())) throw new Error("runtime deps unavailable"); - const t = await import("@huggingface/transformers"); + 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 }); diff --git a/src/integrations/mcpClient.ts b/src/integrations/mcpClient.ts index f1fc890..8571550 100644 --- a/src/integrations/mcpClient.ts +++ b/src/integrations/mcpClient.ts @@ -295,33 +295,3 @@ export class McpManager { } export const mcpManager = new McpManager(); - -/** A server entry from the official MCP registry (registry.modelcontextprotocol.io). */ -export interface RegistryServer { - name: string; - title?: string; - description?: string; - version?: string; - packages?: { - registryType?: string; - identifier?: string; - version?: string; - runtimeHint?: string; - transport?: { type?: string }; - runtimeArguments?: { type?: string; name?: string; value?: string }[]; - packageArguments?: { type?: string; name?: string; value?: string }[]; - environmentVariables?: { name?: string; description?: string; isRequired?: boolean; isSecret?: boolean }[]; - }[]; -} - -/** - * Search the official MCP registry. Runs in the extension host (not the webview) - * so it isn't blocked by the webview CSP. Empty query returns a default listing. - */ -export async function searchMcpRegistry(query: string, limit = 30): Promise { - const url = `https://registry.modelcontextprotocol.io/v0.1/servers?limit=${limit}${query.trim() ? `&search=${encodeURIComponent(query.trim())}` : ""}`; - const r = await fetch(url); - if (!r.ok) throw new Error(`registry ${r.status}`); - const data = (await r.json()) as { servers?: { server: RegistryServer }[] }; - return (data.servers ?? []).map((s) => s.server); -} diff --git a/src/runtimeDeps.ts b/src/runtimeDeps.ts index 9395c43..80e30be 100644 --- a/src/runtimeDeps.ts +++ b/src/runtimeDeps.ts @@ -19,6 +19,7 @@ import * as fs from "fs"; import * as path from "path"; import * as zlib from "zlib"; import * as crypto from "crypto"; +import { pathToFileURL } from "url"; const REGISTRY = "https://registry.npmjs.org"; @@ -252,6 +253,32 @@ export function ensureRuntimeDeps(): Promise { return readyP; } +/** Resolve a package's ESM entry point from its package.json. */ +function entryOf(pkg: any): string { + const pick = (v: any): string | undefined => { + if (typeof v === "string") return v; + if (v && typeof v === "object") return pick(v.import ?? v.node ?? v.default ?? v.require); + return undefined; + }; + return pick(pkg.exports?.["."] ?? pkg.exports) || pkg.module || pkg.main || "index.js"; +} + +/** + * Import a runtime dep. In production the extension host's ESM resolver can't + * see runtime-deps/node_modules (the CJS require hook doesn't apply to + * import()), so we resolve the entry file ourselves and import it by file URL. + */ +export async function importRuntimeDep(name: string): Promise { + if (!(await ensureRuntimeDeps())) throw new Error("runtime deps unavailable"); + try { + return await import(name); // dev: real node_modules next to us + } catch { /* fall back to runtime-deps dir */ } + if (!rootDir) throw new Error("runtime deps not initialized"); + const dir = path.join(rootDir, "node_modules", ...name.split("/")); + const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")); + return await import(pathToFileURL(path.join(dir, entryOf(pkg))).href); +} + // Self-check: OPENCURSOR_SELFCHECK=1 node -e "require('./dist/extension.js')" if (process.env.OPENCURSOR_SELFCHECK) { const assert = (c: boolean, m: string) => { if (!c) throw new Error("selfcheck: " + m); }; diff --git a/src/stores/conversationStore.ts b/src/stores/conversationStore.ts index 63db2b4..edf9802 100644 --- a/src/stores/conversationStore.ts +++ b/src/stores/conversationStore.ts @@ -35,14 +35,35 @@ const KEY = "ocursor.conversations"; const ACTIVE_KEY = "ocursor.activeConversation"; export class ConversationStore { - constructor(private readonly context: vscode.ExtensionContext) {} + constructor(private readonly context: vscode.ExtensionContext) { + void this.migrateFromGlobal(); + } + + /** workspaceState = per-workspace storage; VS Code scopes it for us. */ + private get state(): vscode.Memento { + return this.context.workspaceState; + } + + /** One-time: move old globalState conversations into this workspace. */ + private async migrateFromGlobal(): Promise { + const old = this.context.globalState.get(KEY); + if (!old?.length || this.state.get(KEY)?.length) { + if (old) await this.context.globalState.update(KEY, undefined); + return; + } + await this.state.update(KEY, old); + const active = this.context.globalState.get(ACTIVE_KEY); + if (active) await this.state.update(ACTIVE_KEY, active); + await this.context.globalState.update(KEY, undefined); + await this.context.globalState.update(ACTIVE_KEY, undefined); + } private all(): Conversation[] { - return this.context.globalState.get(KEY, []); + return this.state.get(KEY, []); } private async persist(list: Conversation[]): Promise { - await this.context.globalState.update(KEY, list); + await this.state.update(KEY, list); } list(): ConversationSummary[] { @@ -57,11 +78,11 @@ export class ConversationStore { } getActiveId(): string | undefined { - return this.context.globalState.get(ACTIVE_KEY); + return this.state.get(ACTIVE_KEY); } async setActiveId(id: string | undefined): Promise { - await this.context.globalState.update(ACTIVE_KEY, id); + await this.state.update(ACTIVE_KEY, id); } async create(personaId?: string): Promise { diff --git a/src/stores/settingsManager.ts b/src/stores/settingsManager.ts index 7d758a0..664bd59 100644 --- a/src/stores/settingsManager.ts +++ b/src/stores/settingsManager.ts @@ -19,7 +19,8 @@ export interface Settings { } export const DEFAULT_SETTINGS: Settings = { - model: "auto", + // Auto hidden for now (bring back later): "" resolves to first enabled model. + model: "", // 0 = don't send max_tokens; the model decides when to stop. maxResponseLength: 0, enableWorkspaceContext: true, diff --git a/src/ui/settingsPanel.ts b/src/ui/settingsPanel.ts index 8d7d600..84c4a20 100644 --- a/src/ui/settingsPanel.ts +++ b/src/ui/settingsPanel.ts @@ -13,7 +13,7 @@ import { listModels } from "../agent/provider"; import { renderWebviewHtml } from "./webviewHtml"; import { FeatureStore, MODEL_CATALOG } from "../stores/featureStore"; import { listRules, listSkills } from "../context/workspaceContext"; -import { mcpManager, searchMcpRegistry } from "../integrations/mcpClient"; +import { mcpManager } from "../integrations/mcpClient"; import { BUILTIN_PERSONAS } from "../agent/personas"; import { getStatus, onIndexStatus, buildIndex, deleteIndex, EMBED_MODELS } from "../agent/semanticIndex"; import { indexDocSource, deleteDocIndex, onDocsStatus, getDocsStatus, getDocLogs, type DocSource } from "../agent/docsIndex"; @@ -234,15 +234,6 @@ export class SettingsPanel { case "resetStorage": await this._resetStorage(); break; - case "mcpRegistrySearch": { - try { - const servers = await searchMcpRegistry(message.query || ""); - this._panel.webview.postMessage({ type: "mcpRegistryResults", servers }); - } catch (err: any) { - this._panel.webview.postMessage({ type: "mcpRegistryResults", servers: [], error: String(err?.message || err) }); - } - break; - } case "saveProviderKey": await this.settingsManager.setProviderKey(message.providerId, message.apiKey ?? ""); this.featureStore.notifyChanged(); diff --git a/src/ui/sidebarProvider.ts b/src/ui/sidebarProvider.ts index 56a63e3..7c30b95 100644 --- a/src/ui/sidebarProvider.ts +++ b/src/ui/sidebarProvider.ts @@ -839,7 +839,8 @@ export class SidebarProvider implements vscode.WebviewViewProvider { /** Whether a model id maps to a managed local llama.cpp model. */ private _localModel(modelId: string) { - return this.featureStore.get().llamacppModels.find((m) => m.id === modelId); + const bare = stripModelScope(modelId); + return this.featureStore.get().llamacppModels.find((m) => m.id === modelId || m.id === bare); } /** @@ -1021,8 +1022,16 @@ export class SidebarProvider implements vscode.WebviewViewProvider { const judge = features.autoJudgeModel || candidates[0]; try { const jprov = await this._resolveProviderForModel(judge); - const picked = await pickModel(jprov.baseUrl, jprov.apiKey, judge, candidates, task, jprov.anthropic); + // Local judge whose server isn't running would need a full model load just + // to route — not worth it; fall back to the first candidate instead. + const judgeIsLocal = !!this._localModel(judge); + if (judgeIsLocal && !isRunning(stripModelScope(judge))) return candidates[0]; + if (!jprov.oauthKind && !jprov.baseUrl) return candidates[0]; // unroutable judge + const picked = await pickModel(jprov.baseUrl, jprov.apiKey, jprov.model, candidates, task, jprov.anthropic, jprov.oauthKind); if (picked && candidates.includes(picked)) return picked; + // Judge may reply with a bare model id (no provider scope) — match it. + const scoped = candidates.find((c) => stripModelScope(c) === stripModelScope(picked)); + if (scoped) return scoped; } catch { // fall through to default } @@ -1079,12 +1088,13 @@ export class SidebarProvider implements vscode.WebviewViewProvider { const allIds = fetched.flatMap((f) => f.ids); const modelList = this._buildModelList(fetched); - // Selected model vanished (e.g. account disabled) -> fall back to auto. + // Selected model vanished (e.g. account disabled) or is "auto" (hidden for + // now) -> fall back to the first enabled model. const settings = this.settingsManager.getSettings(); - if (settings.model && settings.model !== "auto" && !modelList.some((m) => m.id === settings.model)) { - settings.model = "auto"; + if (settings.model === "auto" || (settings.model && !modelList.some((m) => m.id === settings.model))) { + settings.model = modelList[0]?.id || ""; await this.settingsManager.saveSettings(settings); - this._view?.webview.postMessage({ type: "modelSelected", model: "auto" }); + this._view?.webview.postMessage({ type: "modelSelected", model: settings.model }); } this._view?.webview.postMessage({ type: "modelsFetched", models: allIds, modelList }); } @@ -1165,10 +1175,12 @@ export class SidebarProvider implements vscode.WebviewViewProvider { if (edit?.mode) this._currentMode = edit.mode as Mode; const settings = this.settingsManager.getSettings(); - // Auto mode: let a judge model pick the best enabled model for this task. let modelId = settings.model; - if (modelId === "auto") { - modelId = await this._resolveAutoModel(text); + // Auto mode is hidden for now; a lingering "auto" selection (or empty) + // resolves to the first enabled model. (Judge-based routing kept in + // _resolveAutoModel for when Auto returns.) + if (modelId === "auto" || !modelId) { + modelId = this._buildModelList([]).find((m) => m.id !== "auto")?.id || modelId; } // Resolve connection details without starting a local server yet — we want // the chat UI to show a "loading model" state while it boots (below). @@ -1286,15 +1298,19 @@ export class SidebarProvider implements vscode.WebviewViewProvider { // Local model not yet running → boot it now, showing a loading state in the // chat (selecting a model never loads it; only sending a message does). const local = this._localModel(modelId); - if (local && !isRunning(local.id)) { - emit({ type: "shell-notify", message: `Loading ${local.name}…` }); - try { - await ensureLoaded(local, features.llamacppConfig); - } catch (e: any) { - emit({ type: "error", message: `Failed to load ${local.name}: ${e?.message || e}` }); - emit({ type: "run-status", status: "error" }); - return; // `finally` clears the session + persists. + if (local) { + if (!isRunning(local.id)) { + emit({ type: "shell-notify", message: `Loading ${local.name}…` }); + try { + await ensureLoaded(local, features.llamacppConfig); + } catch (e: any) { + emit({ type: "error", message: `Failed to load ${local.name}: ${e?.message || e}` }); + emit({ type: "run-status", status: "error" }); + return; // `finally` clears the session + persists. + } } + // The server binds a random port each load — resolve the URL only now. + prov.baseUrl = serverUrlFor(local, features.llamacppConfig); } // Generate a short AI title once the provider/server is ready (local diff --git a/webview-ui/settings/App.tsx b/webview-ui/settings/App.tsx index 706645d..c7ddb45 100644 --- a/webview-ui/settings/App.tsx +++ b/webview-ui/settings/App.tsx @@ -77,7 +77,7 @@ const SECTION_KEYWORDS: Partial> = { behavior: "workspace context file reading terminal tools auto edits approval allow deny ask review policy allowlist denylist commands mcp web", personas: "persona system prompt custom", rules: "rules skills subagents", - mcp: "mcp tools servers marketplace", + mcp: "mcp tools servers", hooks: "hooks events commands", indexing: "codebase index embedding docs semantic sync", advanced: "system prompt custom instructions", @@ -730,6 +730,7 @@ export function App() { setFeatures({ autoGenerateTitles: v })} /> + {/* Auto model (judge routing) hidden for now — bring back later. features.enabledModels.includes(m.id))} @@ -739,6 +740,7 @@ export function App() { style={{ maxWidth: 240 }} /> + */}
Notifications
diff --git a/webview-ui/settings/panels/LlamacppPanel.tsx b/webview-ui/settings/panels/LlamacppPanel.tsx index 4bdcf65..cc286f7 100644 --- a/webview-ui/settings/panels/LlamacppPanel.tsx +++ b/webview-ui/settings/panels/LlamacppPanel.tsx @@ -220,7 +220,7 @@ export function LlamacppPanel({ {m.name} {isLoading && loading…} - {isRunning && !isLoading && running :{m.port}} + {isRunning && !isLoading && running} ); diff --git a/webview-ui/sidebar/sidebar.css b/webview-ui/sidebar/sidebar.css index 849f369..322c313 100644 --- a/webview-ui/sidebar/sidebar.css +++ b/webview-ui/sidebar/sidebar.css @@ -676,7 +676,7 @@ body { .pill .cd { width: 11px; height: 11px; } .pill span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.mode-dropdown { position: absolute; bottom: calc(100% + 6px); left: 0; min-width: 150px; background: var(--dropdown-bg); border: 1px solid var(--dropdown-border); border-radius: 8px; padding: 3px; box-shadow: 0 8px 24px var(--shadow); z-index: 60; animation: pop-up .12s ease; } +.mode-dropdown { position: fixed; min-width: 150px; background: var(--dropdown-bg); border: 1px solid var(--dropdown-border); border-radius: 8px; padding: 3px; box-shadow: 0 8px 24px var(--shadow); z-index: 200; animation: pop-up .12s ease; } .mode-item { display: flex; align-items: center; gap: 7px; height: 26px; padding: 0 7px; border-radius: 5px; font-size: 12px; color: var(--fg); cursor: pointer; } .mode-item:hover { background: var(--hover); } .mode-item .mi-icon { display: inline-flex; flex: 0 0 auto; color: var(--fg-dim); }