From aa52079cc7dffc5d090ef072d24b88e6ca647ef4 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sat, 24 Oct 2026 06:30:37 +0000 Subject: [PATCH] Address review feedback on Local Models - Only delete LM Studio models on a local server with a single variant - Count every loaded LM Studio copy in memory totals - Cap the saved context window at the model's limit on Use with Cline - Clear the runtime model list cache after downloads and deletes - Hide the local model chip in SSH sessions - Ignore stale status refreshes and stale ClinePass lookups - Add fetchCurrentUserPlan to RpcClineAccountService - Label the Ollama response timeout as the default --- .../desktop-app/sidecar/local-models.ts | 4 ++++ .../sidecar/local-runtimes/lmstudio.ts | 18 ++++++++++++++++-- .../components/views/chat/chat-input-bar.tsx | 6 +++++- .../components/views/settings/account-view.tsx | 12 +++++++++++- .../settings/local-model-details-dialog.tsx | 2 +- .../views/settings/local-models-view.tsx | 16 +++++++++++++--- docs/usage/cline-desktop-local-models.mdx | 2 +- sdk/packages/core/src/account/rpc.ts | 7 +++++++ sdk/packages/core/src/index.ts | 1 + 9 files changed, 59 insertions(+), 9 deletions(-) diff --git a/apps/examples/desktop-app/sidecar/local-models.ts b/apps/examples/desktop-app/sidecar/local-models.ts index 49de8f317a..e64d7285d0 100644 --- a/apps/examples/desktop-app/sidecar/local-models.ts +++ b/apps/examples/desktop-app/sidecar/local-models.ts @@ -2,6 +2,7 @@ import { execFile } from "node:child_process"; import { statfs } from "node:fs/promises"; import os from "node:os"; import { promisify } from "node:util"; +import { clearPublicModelsCatalogCache } from "@cline/core"; import { isLocalRuntime, LOCAL_MODEL_PULL_PROGRESS_EVENT, @@ -213,6 +214,8 @@ function startPull( }; } finally { activePulls.delete(key); + // The composer's model list comes from a cached copy of the runtime's. + clearPublicModelsCatalogCache(); broadcastEvent(ctx, LOCAL_MODEL_PULL_PROGRESS_EVENT, final); } })(); @@ -264,6 +267,7 @@ export async function handleLocalModelsCommand( return {}; case "delete_local_model": await backend.delete(readModel(args)); + clearPublicModelsCatalogCache(); return {}; case "show_local_model": return await backend.show(readModel(args)); diff --git a/apps/examples/desktop-app/sidecar/local-runtimes/lmstudio.ts b/apps/examples/desktop-app/sidecar/local-runtimes/lmstudio.ts index 474946aa9a..ffca480fa8 100644 --- a/apps/examples/desktop-app/sidecar/local-runtimes/lmstudio.ts +++ b/apps/examples/desktop-app/sidecar/local-runtimes/lmstudio.ts @@ -37,6 +37,7 @@ interface LmStudioModel { reasoning?: unknown; }; loaded_instances?: { id: string; config?: Record }[]; + variants?: string[]; } /** A job from `POST /api/v1/models/download` and its status endpoint. */ @@ -142,6 +143,13 @@ export function isNestedDir(parent: string, dir: string, depth: number) { const hubModelsDir = (home: string) => path.join(home, "hub", "models"); +/** + * Deleting removes the model's whole source repos, which also hold any other + * downloaded variants, and only makes sense when those files are on this computer. + */ +const canDelete = (model: LmStudioModel) => + isLocalUrl(readServerUrl()) && (model.variants?.length ?? 0) <= 1; + /** * Models downloaded from LM Studio Hub come with a manifest naming the * Hugging Face repos their files were downloaded from. LM Studio has no API @@ -251,14 +259,15 @@ export const lmstudioBackend: LocalRuntimeBackend = { quantization: model.quantization?.name ?? "", capabilities: readCapabilities(model), contextLength: model.max_context_length ?? 0, - deletable: !!(await readHubSources(home, model.key)), + deletable: + canDelete(model) && !!(await readHubSources(home, model.key)), })), ), running: models .filter((model) => model.loaded_instances?.length) .map((model) => ({ name: model.key, - sizeBytes: model.size_bytes, + sizeBytes: model.size_bytes * (model.loaded_instances?.length ?? 1), contextLength: Number(model.loaded_instances?.[0]?.config?.context_length) || 0, })), @@ -324,6 +333,11 @@ export const lmstudioBackend: LocalRuntimeBackend = { }, async delete(model) { + if (!canDelete(await findModel(model))) { + throw new Error( + "Cline can't delete this model. Remove it from LM Studio.", + ); + } const home = await readHome(); const sources = await readHubSources(home, model); if (!sources) { diff --git a/apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.tsx b/apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.tsx index 2b9860e19d..b351d7d093 100644 --- a/apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.tsx +++ b/apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.tsx @@ -84,6 +84,7 @@ import { import type { ProviderModel } from "@/lib/provider-schema"; import { startStreamingTranscription } from "@/lib/streaming-transcription"; import { cn } from "@/lib/utils"; +import { LOCAL_WORKSPACE_ENVIRONMENT_ID } from "@/lib/workspace-paths"; import { LocalModelIndicator } from "./local-model-indicator"; import { TokenUsageRing } from "./token-usage-ring"; import { WorkspaceSelector as WorkspaceSelectorImpl } from "./workspace-selector"; @@ -1707,7 +1708,10 @@ function ChatInputBarImpl({ ))} - {executionTarget !== "cloud" && isLocalRuntime(localRuntime) ? ( + {/* The chip controls this computer's runtime, which SSH sessions don't use. */} + {executionTarget !== "cloud" && + environmentId === LOCAL_WORKSPACE_ENVIRONMENT_ID && + isLocalRuntime(localRuntime) ? ( ([]); const [clinePassModels, setClinePassModels] = useState(null); + const clinePassGenerationRef = useRef(0); const [overviewLoading, setOverviewLoading] = useState(true); const [overviewError, setOverviewError] = useState(null); // Signed out is an expected state carried by a typed sidecar result (or a @@ -236,6 +237,7 @@ export function AccountView() { setBalance(null); setOrganizationBalance(null); setOrganizations([]); + clinePassGenerationRef.current += 1; setClinePassModels(null); setUsageTransactions([]); setUsageLoaded(false); @@ -260,7 +262,15 @@ export function AccountView() { return; } // The subscription card is optional, so it neither delays nor breaks the page. - void fetchClinePassModels().then(setClinePassModels, () => {}); + const generation = ++clinePassGenerationRef.current; + void fetchClinePassModels().then( + (models) => { + if (clinePassGenerationRef.current === generation) { + setClinePassModels(models); + } + }, + () => {}, + ); const [balanceData, orgsData] = await Promise.all([ fetchAccountBalance(), fetchAccountOrganizations(), diff --git a/apps/examples/desktop-app/webview/components/views/settings/local-model-details-dialog.tsx b/apps/examples/desktop-app/webview/components/views/settings/local-model-details-dialog.tsx index 22f113fe01..6a4972e14c 100644 --- a/apps/examples/desktop-app/webview/components/views/settings/local-model-details-dialog.tsx +++ b/apps/examples/desktop-app/webview/components/views/settings/local-model-details-dialog.tsx @@ -290,7 +290,7 @@ export function LocalModelDetailsDialog({ ) : null} diff --git a/apps/examples/desktop-app/webview/components/views/settings/local-models-view.tsx b/apps/examples/desktop-app/webview/components/views/settings/local-models-view.tsx index 3404ac5fd9..bc1034157a 100644 --- a/apps/examples/desktop-app/webview/components/views/settings/local-models-view.tsx +++ b/apps/examples/desktop-app/webview/components/views/settings/local-models-view.tsx @@ -298,6 +298,7 @@ function RuntimePicker({ export function LocalModelsContent() { const [runtime, setRuntime] = useState(readPreferredRuntime); const runtimeRef = useRef(runtime); + const refreshGenerationRef = useRef(0); const [status, setStatus] = useState(); const [refreshing, setRefreshing] = useState(false); const [pulls, setPulls] = useState>( @@ -328,6 +329,7 @@ export function LocalModelsContent() { const refresh = useCallback(async ({ quiet = false } = {}) => { if (!quiet) setRefreshing(true); + const generation = ++refreshGenerationRef.current; try { const requested = runtimeRef.current; // Without a saved choice, the backend picks whichever runtime is set up. @@ -335,7 +337,10 @@ export function LocalModelsContent() { "get_local_models_status", requested ? { runtime: requested } : undefined, ); - if (requested !== runtimeRef.current) { + if ( + requested !== runtimeRef.current || + generation !== refreshGenerationRef.current + ) { return; } if (!requested) { @@ -491,8 +496,13 @@ export function LocalModelsContent() { }), ); - const switchToModel = (model: string, contextWindow: number) => + const switchToModel = (model: string, preferredContextWindow: number) => runAction(model, "using", async () => { + // Runtimes won't load past the model's own limit, so chat mustn't assume more. + const limit = installed.get(model)?.contextLength; + const contextWindow = limit + ? Math.min(preferredContextWindow, limit) + : preferredContextWindow; const provider = shownRuntime; await desktopClient.invoke("save_provider_settings", { provider, @@ -748,7 +758,7 @@ export function LocalModelsContent() { title={ installedModel.deletable ? undefined - : `Only models downloaded from LM Studio Hub can be deleted here. Remove this one from ${runtimeName}.` + : `Cline can only delete LM Studio Hub models with one variant, on this computer. Remove this one in ${runtimeName}.` } type="button" variant="ghost" diff --git a/docs/usage/cline-desktop-local-models.mdx b/docs/usage/cline-desktop-local-models.mdx index d64dfa4231..17241c7c91 100644 --- a/docs/usage/cline-desktop-local-models.mdx +++ b/docs/usage/cline-desktop-local-models.mdx @@ -192,7 +192,7 @@ To change an LM Studio model's sampling or load defaults, open **My Models** in Click **Delete** on a model, then confirm, to remove it from your disk. The confirmation shows how much space you'll get back. If the model is loaded, Cline unloads it first. You can download it again at any time. -With LM Studio, Cline can delete models downloaded from the LM Studio Hub, which includes every model you download from this page. Remove other models, such as ones you added from Hugging Face in LM Studio, from LM Studio's **My Models**. +With LM Studio, Cline can delete models downloaded from the LM Studio Hub, which includes every model you download from this page, when LM Studio runs on this computer. Remove other models, such as ones you added from Hugging Face or models with more than one downloaded variant, from LM Studio's **My Models**. ## Which models are recommended diff --git a/sdk/packages/core/src/account/rpc.ts b/sdk/packages/core/src/account/rpc.ts index 0fd3c7125b..4effb753ca 100644 --- a/sdk/packages/core/src/account/rpc.ts +++ b/sdk/packages/core/src/account/rpc.ts @@ -182,6 +182,13 @@ export class RpcClineAccountService implements ClineAccountOperations { }); } + public async fetchCurrentUserPlan(): Promise { + return this.request({ + action: "clineAccount", + operation: "fetchCurrentUserPlan", + }); + } + private async request(request: ClineAccountActionRequest): Promise { const response = await this.executor.runProviderAction(request); return response.result as T; diff --git a/sdk/packages/core/src/index.ts b/sdk/packages/core/src/index.ts index 4cbd22a836..b663ee5185 100644 --- a/sdk/packages/core/src/index.ts +++ b/sdk/packages/core/src/index.ts @@ -1066,6 +1066,7 @@ export { export { clearLiveModelsCatalogCache, clearPrivateModelsCatalogCache, + clearPublicModelsCatalogCache, DEFAULT_MODELS_CATALOG_URL, getLiveModelsCatalog, getProviderConfig,