diff --git a/.changeset/add-anaconda-desktop-provider.md b/.changeset/add-anaconda-desktop-provider.md new file mode 100644 index 00000000000..e399797ce42 --- /dev/null +++ b/.changeset/add-anaconda-desktop-provider.md @@ -0,0 +1,7 @@ +--- +"@kilocode/cli": minor +"@kilocode/sdk": minor +"kilo-code": minor +--- + +Connect to a local Anaconda Desktop text-generation model server from the CLI or VS Code. diff --git a/packages/kilo-docs/lib/nav/ai-providers.ts b/packages/kilo-docs/lib/nav/ai-providers.ts index 0711ee81510..cfff0b8038b 100644 --- a/packages/kilo-docs/lib/nav/ai-providers.ts +++ b/packages/kilo-docs/lib/nav/ai-providers.ts @@ -56,6 +56,10 @@ export const AiProvidersNav: NavSection[] = [ { href: "/ai-providers/ollama", children: "Ollama" }, { href: "/ai-providers/lmstudio", children: "LM Studio" }, { href: "/ai-providers/atomic-chat", children: "Atomic Chat" }, + { + href: "/ai-providers/anaconda-desktop", + children: "Anaconda Desktop", + }, { href: "/ai-providers/vscode-lm", children: "VS Code LM API" }, { href: "/ai-providers/openai-compatible", diff --git a/packages/kilo-docs/pages/ai-providers/anaconda-desktop.md b/packages/kilo-docs/pages/ai-providers/anaconda-desktop.md new file mode 100644 index 00000000000..6f26b611e64 --- /dev/null +++ b/packages/kilo-docs/pages/ai-providers/anaconda-desktop.md @@ -0,0 +1,92 @@ +--- +title: "Using Anaconda Desktop with Kilo Code | Local Models" +description: "Connect Kilo Code to a local Anaconda Desktop text-generation model server from the TUI or VS Code." +sidebar_label: Anaconda Desktop +--- + +# Using Anaconda Desktop With Kilo Code + +Kilo Code can discover the text-generation model served by [Anaconda Desktop](https://www.anaconda.com/products/desktop) and connect to its local OpenAI-compatible endpoint. Kilo imports the server connection for you, so you do not need to copy an API key or configure a base URL manually. + +**Official documentation:** [Anaconda Desktop](https://www.anaconda.com/docs/tools/anaconda-desktop/key-features) + +## Supported Platforms + +Anaconda Desktop and Kilo must run on the same supported computer. + +| Operating system | Supported installation | +|---|---| +| Windows | Windows 11, x86-64 | +| macOS | macOS 13 or later, Apple Silicon | +| Linux | Debian or Ubuntu, x86-64 or ARM64 | + +Remote backends, remote-only model endpoints, and non-interactive or headless setup are not supported. Complete setup in the Kilo TUI or VS Code on the computer running Anaconda Desktop. + +## Set Up Anaconda Desktop + +1. Download and install Anaconda Desktop from the [official product page](https://www.anaconda.com/products/desktop). See Anaconda's [installation guide](https://www.anaconda.com/docs/tools/anaconda-desktop/install-desktop) for platform-specific steps. +2. Open Anaconda Desktop and sign in with your Anaconda account or your organization's assigned credentials. +3. Select **Model Catalog** and filter for a **Text Generation** model. Prefer a model tagged **Tool Calling** for full Kilo agent functionality. +4. Select a quantization that fits your computer, then click **Download**. Anaconda's [model catalog guide](https://www.anaconda.com/docs/tools/anaconda-desktop/model-catalog) explains model types, hardware requirements, and quantization choices. +5. Select **Model Servers**, choose the downloaded model and file, and start its server. Enable tool calling when the selected model and server configuration support it. + +{% callout type="note" %} +Kilo only discovers Desktop state and connects to an existing server. Downloading or deleting models and creating, switching, starting, stopping, or deleting servers must be done in Anaconda Desktop. See Anaconda's [model server guide](https://www.anaconda.com/docs/tools/anaconda-desktop/servers). +{% /callout %} + +## Connect Kilo Code + +{% tabs %} +{% tab label="TUI" %} + +1. Run `/connect` in the Kilo TUI. +2. Select **Anaconda Desktop**. +3. Follow the setup dialog. Kilo can open Anaconda Desktop; after making changes there, return and choose **Check again**. +4. When the model server is ready, choose **Connect** to import its connection and make the served model available in the model picker. + +To refresh an existing connection, run `/connect`, select **Anaconda Desktop**, and choose **connect / refresh now** after changing the model, server address or port, or server API key in Desktop. Kilo re-discovers the active server and replaces its stored model and connection information. + +{% /tab %} +{% tab label="VS Code" %} + +1. Open Kilo Code **Settings** using the gear icon and select **Providers**. +2. Add **Anaconda Desktop**. No manual API-key field is shown. +3. Follow the setup dialog. Kilo can open Anaconda Desktop; after making changes there, return and select **Check again**. +4. When the model server is ready, select **Connect** to import its connection and refresh the model picker. + +For an existing connection, open **Settings**, select **Providers**, and select **Manage / Refresh** for Anaconda Desktop after changing the model, server address or port, or server API key in Desktop. + +{% /tab %} +{% /tabs %} + +## Tool Calling + +When Desktop reports that the model server supports tool calling, Kilo allows you to connect without an additional warning. Tool calling lets the model use Kilo's tools to inspect files, edit code, and run commands. + +{% callout type="warning" %} +If tool support is unavailable or cannot be detected, Kilo shows a warning and requires confirmation before connecting. You can still use the model for text generation, but normal coding-agent actions are limited and may fail. For the best experience, choose a **Text Generation** model tagged **Tool Calling** and enable the server's tool-call support when available. +{% /callout %} + +## How Keys Are Handled + +Anaconda Desktop uses two separate credentials: + +- **Desktop management key** - Allows local discovery of Desktop models and servers. Kilo reads it from Desktop's configuration only when needed and never copies it into Kilo storage. +- **Inference server key** - Authenticates chat-completion requests to the running model server. Kilo imports this key into its normal provider authentication storage together with the local endpoint and model details. + +Kilo never asks you to paste either key. If the inference server key or endpoint changes, use **Refresh** to import the current values. + +## Disconnect + +In VS Code, open **Settings** > **Providers** and select **Disconnect** for Anaconda Desktop. In a terminal, run `kilo auth logout` and select Anaconda Desktop. Disconnecting removes only Kilo's stored provider authentication and connection metadata. It does not stop Anaconda Desktop, stop the model server, or delete the downloaded model. + +Use Anaconda Desktop itself to stop or change the server. + +## Troubleshooting + +- **Desktop is not detected:** Install it from the [official product page](https://www.anaconda.com/products/desktop), or use **Open Anaconda Desktop** if it is installed but not running. +- **Sign-in is required:** Open Desktop, complete sign-in, and leave the Kilo setup dialog open so it can detect the change. +- **No model is available:** Download a model whose type is **Text Generation**, not an embedding-only model. +- **No server is available:** Start the downloaded model from **Model Servers** in Desktop. +- **The server changed:** Use **Refresh** in Kilo to replace the stored model, endpoint, and inference key. +- **The server is unhealthy:** Check the server status and logs in Desktop, then restart it there before refreshing Kilo. diff --git a/packages/kilo-docs/pages/ai-providers/index.md b/packages/kilo-docs/pages/ai-providers/index.md index ef843690443..48b09f27c7e 100644 --- a/packages/kilo-docs/pages/ai-providers/index.md +++ b/packages/kilo-docs/pages/ai-providers/index.md @@ -34,6 +34,7 @@ Major AI companies offering powerful models via API: Run models on your own hardware for privacy and offline use: - **[Atomic Chat](/docs/ai-providers/atomic-chat)** - Local models with TurboQuant inference and auto-discovery in Kilo Code +- **[Anaconda Desktop](/docs/ai-providers/anaconda-desktop)** - Discover and connect to a local text-generation model server - **[Ollama](/docs/ai-providers/ollama)** - Easy local model management - **[LM Studio](/docs/ai-providers/lmstudio)** - Desktop app for local models - **[OpenAI Compatible](/docs/ai-providers/openai-compatible)** - Any OpenAI-compatible endpoint diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/anaconda-desktop/limited-tools-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/anaconda-desktop/limited-tools-chromium-linux.png new file mode 100644 index 00000000000..80d30b747ff --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/anaconda-desktop/limited-tools-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79039cc251eaee4b01ad5eace1bf9fc28a85184fb98695694cc6f3c93e734813 +size 177 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/anaconda-desktop/not-installed-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/anaconda-desktop/not-installed-chromium-linux.png new file mode 100644 index 00000000000..80d30b747ff --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/anaconda-desktop/not-installed-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79039cc251eaee4b01ad5eace1bf9fc28a85184fb98695694cc6f3c93e734813 +size 177 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/anaconda-desktop/ready-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/anaconda-desktop/ready-chromium-linux.png new file mode 100644 index 00000000000..80d30b747ff --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/anaconda-desktop/ready-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79039cc251eaee4b01ad5eace1bf9fc28a85184fb98695694cc6f3c93e734813 +size 177 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/anaconda-desktop/waiting-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/anaconda-desktop/waiting-chromium-linux.png new file mode 100644 index 00000000000..80d30b747ff --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/anaconda-desktop/waiting-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79039cc251eaee4b01ad5eace1bf9fc28a85184fb98695694cc6f3c93e734813 +size 177 diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index ce7301c2770..cb66905c5fb 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -188,6 +188,8 @@ - +- + - - diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 118ae7243ec..883f744b224 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -144,6 +144,7 @@ import { resolveStoredKey, } from "./provider-actions" import type { StoredProviderKey } from "./provider-actions" +import { AnacondaDesktopBridge } from "./anaconda-desktop/bridge" import { fetchOpenAIModels, FetchModelsError } from "./shared/fetch-models" import type { Agent } from "@kilocode/sdk/v2/client" import { configFeatures } from "./features" @@ -343,6 +344,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private readonly sandboxTransitions = new Map>() private readonly revisions = new Map() private readonly refreshes = new Map() + private readonly anacondaDesktop = new AnacondaDesktopBridge() private sessionStatusMap = new Map() // Latest status used for destructive config warnings. private sessionDirectories = new Map() // Per-session directory overrides, such as Agent Manager worktrees. private readonly aborts = new SessionAbort() @@ -1015,6 +1017,18 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper case "saveCustomProvider": await this.handleProviderAction(message) break + case "anacondaDesktopStatus": + case "anacondaDesktopOpen": + case "anacondaDesktopSync": + case "cancelAnacondaDesktopRequest": + await this.anacondaDesktop.handle(message, { + client: this.client, + directory: this.getWorkspaceDirectory(), + post: (reply) => this.postMessage(reply), + refresh: () => this.fetchAndSendProviders(), + error: getErrorMessage, + }) + break case "fetchCustomProviderModels": this.handleFetchCustomProviderModels(message).catch((e) => console.error("[Kilo New] fetchCustomProviderModels failed:", e), @@ -3856,6 +3870,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.trackedSessionIds.clear() this.syncedChildSessions.clear() this.sessionDirectories.clear() + this.anacondaDesktop.dispose() this.aborts.clear() this.sessionStatusMap.clear() this.ignoreController?.dispose() diff --git a/packages/kilo-vscode/src/anaconda-desktop/bridge.ts b/packages/kilo-vscode/src/anaconda-desktop/bridge.ts new file mode 100644 index 00000000000..9d6676b5426 --- /dev/null +++ b/packages/kilo-vscode/src/anaconda-desktop/bridge.ts @@ -0,0 +1,94 @@ +import type { KiloClient } from "@kilocode/sdk/v2" +import type { + AnacondaDesktopAction, + AnacondaDesktopExtensionMessage, + AnacondaDesktopWebviewMessage, +} from "../shared/anaconda-desktop-messages" + +interface Context { + client?: KiloClient | null + directory: string + post: (message: AnacondaDesktopExtensionMessage) => void + refresh: () => Promise + error: (error: unknown) => string +} + +type Request = Exclude + +function action(message: Request): AnacondaDesktopAction { + if (message.type === "anacondaDesktopStatus") return "status" + if (message.type === "anacondaDesktopOpen") return "open" + return "sync" +} + +export class AnacondaDesktopBridge { + private readonly requests = new Map() + + async handle(message: AnacondaDesktopWebviewMessage, ctx: Context) { + if (message.type === "cancelAnacondaDesktopRequest") { + this.requests.get(message.requestId)?.abort() + this.requests.delete(message.requestId) + return + } + + if (!ctx.client) { + ctx.post({ + type: "anacondaDesktopActionError", + requestId: message.requestId, + action: action(message), + message: "Not connected to CLI backend", + }) + return + } + + const ctrl = new AbortController() + this.requests.set(message.requestId, ctrl) + try { + if (message.type === "anacondaDesktopStatus") { + const response = await ctx.client.anacondaDesktop.status( + { directory: ctx.directory }, + { throwOnError: true, signal: ctrl.signal }, + ) + if (!response.data) throw new Error("Failed to check Anaconda Desktop") + if (ctrl.signal.aborted) return + ctx.post({ type: "anacondaDesktopStatusResult", requestId: message.requestId, status: response.data }) + return + } + + if (message.type === "anacondaDesktopOpen") { + await ctx.client.anacondaDesktop.open({ directory: ctx.directory }, { throwOnError: true, signal: ctrl.signal }) + if (ctrl.signal.aborted) return + ctx.post({ type: "anacondaDesktopOpened", requestId: message.requestId }) + return + } + + const response = await ctx.client.anacondaDesktop.sync( + { + directory: ctx.directory, + acknowledgeToolLimitations: message.acknowledgeToolLimitations, + }, + { throwOnError: true, signal: ctrl.signal }, + ) + if (!response.data) throw new Error("Failed to synchronize Anaconda Desktop") + if (ctrl.signal.aborted) return + await ctx.refresh() + if (ctrl.signal.aborted) return + ctx.post({ type: "anacondaDesktopSynced", requestId: message.requestId, status: response.data }) + } catch (error) { + if (ctrl.signal.aborted) return + ctx.post({ + type: "anacondaDesktopActionError", + requestId: message.requestId, + action: action(message), + message: ctx.error(error) || `Failed to ${action(message)} Anaconda Desktop`, + }) + } finally { + if (this.requests.get(message.requestId) === ctrl) this.requests.delete(message.requestId) + } + } + + dispose() { + for (const ctrl of this.requests.values()) ctrl.abort() + this.requests.clear() + } +} diff --git a/packages/kilo-vscode/src/shared/anaconda-desktop-messages.ts b/packages/kilo-vscode/src/shared/anaconda-desktop-messages.ts new file mode 100644 index 00000000000..40325593770 --- /dev/null +++ b/packages/kilo-vscode/src/shared/anaconda-desktop-messages.ts @@ -0,0 +1,36 @@ +import type { AnacondaDesktopStatus } from "@kilocode/sdk/v2/client" + +export type AnacondaDesktopAction = "status" | "open" | "sync" + +export type AnacondaDesktopWebviewMessage = + | { type: "anacondaDesktopStatus"; requestId: string } + | { type: "anacondaDesktopOpen"; requestId: string } + | { + type: "anacondaDesktopSync" + requestId: string + acknowledgeToolLimitations: boolean + } + | { type: "cancelAnacondaDesktopRequest"; requestId: string } + +export type AnacondaDesktopExtensionMessage = + | { + type: "anacondaDesktopStatusResult" + requestId: string + status: AnacondaDesktopStatus + } + | { type: "anacondaDesktopOpened"; requestId: string } + | { + type: "anacondaDesktopSynced" + requestId: string + status: Extract + } + | { + type: "anacondaDesktopActionError" + requestId: string + action: AnacondaDesktopAction + message: string + } + +export type AnacondaDesktopRequest = Exclude +export type AnacondaDesktopResult = Exclude +export type AnacondaDesktopError = Extract diff --git a/packages/kilo-vscode/tests/unit/anaconda-desktop-actions.test.ts b/packages/kilo-vscode/tests/unit/anaconda-desktop-actions.test.ts new file mode 100644 index 00000000000..274542b34f4 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/anaconda-desktop-actions.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "bun:test" +import { AnacondaDesktopBridge } from "../../src/anaconda-desktop/bridge" +import { createAnacondaDesktopAction } from "../../webview-ui/src/utils/anaconda-desktop-action" +import type { ExtensionMessage, WebviewMessage } from "../../webview-ui/src/types/messages" + +const ready = { + type: "ready" as const, + serverID: "server", + models: [{ id: "model", name: "Local Model" }], + context: 32768, + toolcall: "unknown" as const, +} + +describe("AnacondaDesktopBridge", () => { + it("forwards explicit tool acknowledgement", async () => { + const calls: unknown[][] = [] + let refreshed = false + const bridge = new AnacondaDesktopBridge() + await bridge.handle( + { type: "anacondaDesktopSync", requestId: "sync", acknowledgeToolLimitations: true }, + { + client: { + anacondaDesktop: { + sync: async (...args: unknown[]) => { + calls.push(args) + return { data: ready } + }, + }, + } as never, + directory: "/workspace", + post: () => {}, + refresh: async () => { + refreshed = true + }, + error: String, + }, + ) + + expect(calls[0]?.[0]).toEqual({ directory: "/workspace", acknowledgeToolLimitations: true }) + expect(refreshed).toBe(true) + }) + + it("aborts cancelled requests and suppresses their result", async () => { + const posts: unknown[] = [] + const bridge = new AnacondaDesktopBridge() + const client = { + anacondaDesktop: { + status: (_: unknown, opts: { signal: AbortSignal }) => + new Promise((_, reject) => opts.signal.addEventListener("abort", () => reject(new Error("aborted")))), + }, + } + const ctx = { + client: client as never, + directory: "/workspace", + post: (message: unknown) => posts.push(message), + refresh: async () => {}, + error: String, + } + const request = bridge.handle({ type: "anacondaDesktopStatus", requestId: "request" }, ctx) + + await bridge.handle({ type: "cancelAnacondaDesktopRequest", requestId: "request" }, ctx) + await request + expect(posts).toEqual([]) + }) +}) + +function transport() { + const sent: WebviewMessage[] = [] + const listeners = new Set<(message: ExtensionMessage) => void>() + return { + sent, + postMessage: (message: WebviewMessage) => sent.push(message), + onMessage: (handler: (message: ExtensionMessage) => void) => { + listeners.add(handler) + return () => listeners.delete(handler) + }, + receive: (message: ExtensionMessage) => listeners.forEach((handler) => handler(message)), + } +} + +it("correlates results and cancels pending webview requests", () => { + const vscode = transport() + const action = createAnacondaDesktopAction(vscode) + const seen: string[] = [] + const requestId = action.send( + { type: "anacondaDesktopStatus" }, + { onStatus: (message) => seen.push(message.status.type) }, + ) + + vscode.receive({ + type: "anacondaDesktopStatusResult", + requestId, + status: { type: "no-running-server", downloadedModels: 1 }, + }) + const cancelled = action.send({ type: "anacondaDesktopOpen" }) + action.clear(cancelled) + + expect(seen).toEqual(["no-running-server"]) + expect(vscode.sent.at(-1)).toEqual({ type: "cancelAnacondaDesktopRequest", requestId: cancelled }) + action.dispose() +}) diff --git a/packages/kilo-vscode/tests/unit/message-contract.test.ts b/packages/kilo-vscode/tests/unit/message-contract.test.ts index 5b37d2042b7..4aaa92e9eb1 100644 --- a/packages/kilo-vscode/tests/unit/message-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/message-contract.test.ts @@ -22,6 +22,7 @@ const KILO_PROVIDER_UTILS_FILE = path.join(ROOT, "src/kilo-provider-utils.ts") // Some wire types (partUpdated, partsUpdated) live in a file shared by the // extension and webview; the contract checks must include it. const SHARED_STREAM_MESSAGES_FILE = path.join(ROOT, "src/shared/stream-messages.ts") +const ANACONDA_DESKTOP_MESSAGES_FILE = path.join(ROOT, "src/shared/anaconda-desktop-messages.ts") function readFile(filePath: string): string { return fs.readFileSync(filePath, "utf-8") @@ -36,7 +37,7 @@ function readMessagesDir(): string { } function readMessageTypeSources(): string { - return readMessagesDir() + "\n" + readFile(SHARED_STREAM_MESSAGES_FILE) + return [readMessagesDir(), readFile(SHARED_STREAM_MESSAGES_FILE), readFile(ANACONDA_DESKTOP_MESSAGES_FILE)].join("\n") } /** diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AnacondaDesktopDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AnacondaDesktopDialog.tsx new file mode 100644 index 00000000000..06cbdb2cd6b --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AnacondaDesktopDialog.tsx @@ -0,0 +1,301 @@ +import { Button } from "@kilocode/kilo-ui/button" +import { Card, CardDescription, CardTitle } from "@kilocode/kilo-ui/card" +import { useDialog } from "@kilocode/kilo-ui/context/dialog" +import { Dialog } from "@kilocode/kilo-ui/dialog" +import { Spinner } from "@kilocode/kilo-ui/spinner" +import { Tag } from "@kilocode/kilo-ui/tag" +import { showToast } from "@kilocode/kilo-ui/toast" +import type { AnacondaDesktopStatus } from "@kilocode/sdk/v2/client" +import { For, Match, Show, Switch, createMemo, createSignal, onCleanup, onMount } from "solid-js" +import { useLanguage } from "../../context/language" +import { useProvider } from "../../context/provider" +import { useVSCode } from "../../context/vscode" +import { createAnacondaDesktopAction } from "../../utils/anaconda-desktop-action" + +interface AnacondaDesktopDialogProps { + status?: AnacondaDesktopStatus + connected?: boolean +} + +const ID = "anaconda-desktop" + +function AnacondaDesktopDialog(props: AnacondaDesktopDialogProps) { + const dialog = useDialog() + const language = useLanguage() + const provider = useProvider() + const vscode = useVSCode() + const action = createAnacondaDesktopAction(vscode) + const [status, setStatus] = createSignal(props.status) + const [checking, setChecking] = createSignal(false) + const [opening, setOpening] = createSignal(false) + const [syncing, setSyncing] = createSignal(false) + const [error, setError] = createSignal() + const managing = props.connected ?? provider.connected().includes(ID) + + function check() { + if (checking() || opening() || syncing()) return + setChecking(true) + setError() + action.send( + { type: "anacondaDesktopStatus" }, + { + onStatus: (message) => { + setChecking(false) + setStatus(message.status) + }, + onError: (message) => { + setChecking(false) + setError(message.message) + }, + }, + ) + } + + onMount(() => { + if (!props.status) check() + }) + onCleanup(action.dispose) + + function open() { + if (opening() || checking() || syncing()) return + setOpening(true) + setError() + action.send( + { type: "anacondaDesktopOpen" }, + { + onOpened: () => setOpening(false), + onError: (message) => { + setOpening(false) + setError(message.message) + }, + }, + ) + } + + function download(url: string) { + vscode.postMessage({ type: "openExternal", url }) + } + + function sync(acknowledge: boolean) { + if (syncing() || checking() || opening()) return + setSyncing(true) + setError() + action.send( + { type: "anacondaDesktopSync", acknowledgeToolLimitations: acknowledge }, + { + onSynced: () => { + showToast({ + variant: "success", + icon: "circle-check", + title: language.t( + managing ? "provider.anaconda.toast.refreshed.title" : "provider.connect.toast.connected.title", + { provider: "Anaconda Desktop" }, + ), + description: language.t( + managing + ? "provider.anaconda.toast.refreshed.description" + : "provider.connect.toast.connected.description", + { provider: "Anaconda Desktop" }, + ), + }) + dialog.close() + }, + onError: (message) => { + setSyncing(false) + setError(message.message) + }, + }, + ) + } + + function description(current: AnacondaDesktopStatus) { + if (current.type === "unsupported-platform") { + return language.t("provider.anaconda.state.unsupported", { platform: current.platform }) + } + if (current.type === "not-installed") return language.t("provider.anaconda.state.notInstalled") + if (current.type === "not-running") return language.t("provider.anaconda.state.notRunning") + if (current.type === "invalid-config") return language.t("provider.anaconda.state.invalidConfig") + if (current.type === "signed-out") return language.t("provider.anaconda.state.signedOut") + if (current.type === "management-unauthorized") return language.t("provider.anaconda.state.unauthorized") + if (current.type === "management-unavailable") return language.t("provider.anaconda.state.unavailable") + if (current.type === "no-downloaded-model") return language.t("provider.anaconda.state.noModel") + if (current.type === "no-running-server") { + const key = + current.downloadedModels === 1 + ? "provider.anaconda.state.noServer_one" + : "provider.anaconda.state.noServer_other" + return language.t(key, { count: current.downloadedModels }) + } + if (current.type === "inference-unhealthy") return language.t("provider.anaconda.state.unhealthy") + return language.t("provider.anaconda.state.ready") + } + + function label(current: AnacondaDesktopStatus) { + if (current.type === "ready") return language.t("provider.anaconda.status.ready") + if (current.type === "unsupported-platform" || current.type === "not-installed") { + return language.t("provider.anaconda.status.unavailable") + } + if ( + current.type === "not-running" || + current.type === "no-downloaded-model" || + current.type === "no-running-server" + ) { + return language.t("provider.anaconda.status.waiting") + } + return language.t("provider.anaconda.status.attention") + } + + function tools(value: Extract["toolcall"]) { + if (value === "supported") return language.t("provider.anaconda.tools.supported") + if (value === "unsupported") return language.t("provider.anaconda.tools.unsupported") + return language.t("provider.anaconda.tools.unknown") + } + + const ready = createMemo(() => { + const current = status() + return current?.type === "ready" ? current : undefined + }) + const canOpen = createMemo(() => { + const current = status() + if (!current) return false + return ( + current.type === "not-running" || + current.type === "invalid-config" || + current.type === "signed-out" || + current.type === "management-unauthorized" || + current.type === "management-unavailable" || + current.type === "no-downloaded-model" || + current.type === "no-running-server" || + current.type === "inference-unhealthy" + ) + }) + + return ( + +
+ + + {(current) => ( + + {label(current())} + {description(current())} + + )} + + +
+ + {language.t("provider.anaconda.status.checking")} +
+
+
+ + + {(current) => ( + <> + + {current().serverName ?? language.t("provider.anaconda.server")} + +
+
+ {(model) => {model.name}} +
+
+ {language.t("provider.anaconda.context")} + {language.t("provider.anaconda.contextValue", { count: current().context })} +
+
+ {language.t("provider.anaconda.tools")} + {tools(current().toolcall)} +
+
+
+
+ + + {language.t("provider.anaconda.warning.title")} + {language.t("provider.anaconda.warning.description")} + + + + )} +
+ + + {(message) => ( + + {language.t("common.requestFailed")} + {message()} + + )} + + + +
+ + + {language.t( + checking() + ? "provider.anaconda.status.checking" + : syncing() + ? "provider.anaconda.status.syncing" + : "provider.anaconda.status.opening", + )} + +
+
+ +
+ + + + + + + + + + + + + + + + +
+
+
+ ) +} + +export default AnacondaDesktopDialog diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ProviderConnectDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ProviderConnectDialog.tsx index 556ce790f84..5f64842ce3d 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ProviderConnectDialog.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ProviderConnectDialog.tsx @@ -17,6 +17,7 @@ import { isLocalProviderOptionalApiKey, LOCAL_PROVIDER_API_KEY_PLACEHOLDER, } from "../../utils/local-providers" +import AnacondaDesktopDialog from "./AnacondaDesktopDialog" interface ProviderConnectDialogProps { providerID: string @@ -56,6 +57,8 @@ function visible(prompt: Prompt, values: Record) { } const ProviderConnectDialog: Component = (props) => { + if (props.providerID === "anaconda-desktop") return + const dialog = useDialog() const language = useLanguage() const provider = useProvider() diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx index 4964b02f489..c3d9c25d84b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx @@ -71,6 +71,7 @@ const ProvidersTab: Component = () => { } function sourceTag(item: Provider) { + if (item.id === "anaconda-desktop") return language.t("settings.providers.tag.local") const current = source(item) if (current === "env") return language.t("settings.providers.tag.environment") if (current === "api") return language.t("provider.connect.method.apiKey") @@ -261,6 +262,11 @@ const ProvidersTab: Component = () => { {language.t("settings.providers.action.signInChatGPT")} + + +