feat: add Anaconda Desktop provider

This commit is contained in:
Josh Lambert
2026-06-25 22:57:01 -06:00
parent ebba5a6b85
commit 7b2063f354
62 changed files with 4514 additions and 10 deletions
@@ -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.
@@ -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",
@@ -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.
@@ -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
+2
View File
@@ -188,6 +188,8 @@
<!-- packages/opencode/src/provider/transform.ts -->
- <https://vercel.link/ai-gateway-token>
<!-- packages/opencode/src/cli/cmd/providers.ts -->
- <https://www.anaconda.com/products/desktop>
<!-- packages/opencode/src/kilocode/anaconda-desktop/domain.ts -->
- <https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz>
<!-- packages/opencode/src/lsp/server.ts -->
- <https://www.googleapis.com/auth/cloud-platform>
+15
View File
@@ -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<string, Promise<void>>()
private readonly revisions = new Map<string, { id: string; seq: number }>()
private readonly refreshes = new Map<string, number>()
private readonly anacondaDesktop = new AnacondaDesktopBridge()
private sessionStatusMap = new Map<string, SessionStatus["type"]>() // Latest status used for destructive config warnings.
private sessionDirectories = new Map<string, string>() // 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()
@@ -0,0 +1,90 @@
import type { KiloClient } from "@kilocode/sdk/v2"
import type { AnacondaDesktopAction, AnacondaDesktopExtensionMessage, AnacondaDesktopWebviewMessage } from "./messages"
interface Context {
client?: KiloClient | null
directory: string
post: (message: AnacondaDesktopExtensionMessage) => void
refresh: () => Promise<void>
error: (error: unknown) => string
}
type Request = Exclude<AnacondaDesktopWebviewMessage, { type: "cancelAnacondaDesktopRequest" }>
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<string, AbortController>()
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()
}
}
@@ -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<AnacondaDesktopStatus, { type: "ready" }>
}
| {
type: "anacondaDesktopActionError"
requestId: string
action: AnacondaDesktopAction
message: string
}
export type AnacondaDesktopRequest = Exclude<AnacondaDesktopWebviewMessage, { type: "cancelAnacondaDesktopRequest" }>
export type AnacondaDesktopResult = Exclude<AnacondaDesktopExtensionMessage, { type: "anacondaDesktopActionError" }>
export type AnacondaDesktopError = Extract<AnacondaDesktopExtensionMessage, { type: "anacondaDesktopActionError" }>
@@ -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()
})
@@ -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<AnacondaDesktopStatus | undefined>(props.status)
const [checking, setChecking] = createSignal(false)
const [opening, setOpening] = createSignal(false)
const [syncing, setSyncing] = createSignal(false)
const [error, setError] = createSignal<string>()
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<AnacondaDesktopStatus, { type: "ready" }>["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 (
<Dialog title={language.t(managing ? "provider.anaconda.title.manage" : "provider.anaconda.title.connect")} fit>
<div class="dialog-confirm-body" style={{ display: "flex", "flex-direction": "column", gap: "16px" }}>
<Switch>
<Match when={status()}>
{(current) => (
<Card variant={current().type === "ready" ? "success" : "info"}>
<CardTitle variant={current().type === "ready" ? "success" : "info"}>{label(current())}</CardTitle>
<CardDescription>{description(current())}</CardDescription>
</Card>
)}
</Match>
<Match when={checking()}>
<div class="provider-connect-status">
<Spinner />
<span>{language.t("provider.anaconda.status.checking")}</span>
</div>
</Match>
</Switch>
<Show when={ready()}>
{(current) => (
<>
<Card>
<CardTitle icon="server">{current().serverName ?? language.t("provider.anaconda.server")}</CardTitle>
<CardDescription>
<div style={{ display: "flex", "flex-direction": "column", gap: "10px" }}>
<div style={{ display: "flex", "flex-wrap": "wrap", gap: "6px" }}>
<For each={current().models}>{(model) => <Tag size="large">{model.name}</Tag>}</For>
</div>
<div style={{ display: "flex", "justify-content": "space-between", gap: "12px" }}>
<span>{language.t("provider.anaconda.context")}</span>
<strong>{language.t("provider.anaconda.contextValue", { count: current().context })}</strong>
</div>
<div style={{ display: "flex", "justify-content": "space-between", gap: "12px" }}>
<span>{language.t("provider.anaconda.tools")}</span>
<strong>{tools(current().toolcall)}</strong>
</div>
</div>
</CardDescription>
</Card>
<Show when={current().toolcall !== "supported"}>
<Card variant="warning">
<CardTitle variant="warning">{language.t("provider.anaconda.warning.title")}</CardTitle>
<CardDescription>{language.t("provider.anaconda.warning.description")}</CardDescription>
</Card>
</Show>
</>
)}
</Show>
<Show when={error()}>
{(message) => (
<Card variant="error">
<CardTitle variant="error">{language.t("common.requestFailed")}</CardTitle>
<CardDescription>{message()}</CardDescription>
</Card>
)}
</Show>
<Show when={(!!status() && checking()) || opening() || syncing()}>
<div class="provider-connect-status">
<Spinner />
<span>
{language.t(
checking()
? "provider.anaconda.status.checking"
: syncing()
? "provider.anaconda.status.syncing"
: "provider.anaconda.status.opening",
)}
</span>
</div>
</Show>
<div class="dialog-confirm-actions">
<Button variant="ghost" size="large" onClick={() => dialog.close()} disabled={syncing()}>
{language.t("common.cancel")}
</Button>
<Show when={status()?.type === "not-installed"}>
<Button
variant="primary"
size="large"
icon="link"
onClick={() => {
const current = status()
if (current?.type === "not-installed") download(current.downloadURL)
}}
>
{language.t("provider.anaconda.action.download")}
</Button>
</Show>
<Show when={canOpen()}>
<Button variant="secondary" size="large" onClick={open} disabled={checking() || opening() || syncing()}>
{language.t("provider.anaconda.action.open")}
</Button>
</Show>
<Show when={!checking()}>
<Button variant="secondary" size="large" onClick={check} disabled={opening() || syncing()}>
{language.t("provider.anaconda.action.checkAgain")}
</Button>
</Show>
<Show when={ready()?.toolcall === "supported"}>
<Button
variant="primary"
size="large"
onClick={() => sync(false)}
disabled={checking() || opening() || syncing()}
>
{language.t(managing ? "common.refresh" : "common.connect")}
</Button>
</Show>
<Show when={ready()?.toolcall !== "supported" && !!ready()}>
<Button
variant="primary"
size="large"
onClick={() => sync(true)}
disabled={checking() || opening() || syncing()}
>
{language.t("provider.anaconda.action.continue")}
</Button>
</Show>
</div>
</div>
</Dialog>
)
}
export default AnacondaDesktopDialog
@@ -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<string, string>) {
}
const ProviderConnectDialog: Component<ProviderConnectDialogProps> = (props) => {
if (props.providerID === "anaconda-desktop") return <AnacondaDesktopDialog />
const dialog = useDialog()
const language = useLanguage()
const provider = useProvider()
@@ -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")}
</Button>
</Show>
<Show when={item.id === "anaconda-desktop"}>
<Button size="large" variant="ghost" onClick={() => connectProvider(item)}>
{language.t("provider.anaconda.action.manage")}
</Button>
</Show>
<Show when={canDisconnect(item)}>
<Show when={isCustom(item)}>
<Button size="large" variant="ghost" onClick={() => editProvider(item)}>
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "تحقق مرة أخرى",
"provider.anaconda.state.noServer_one":
"يتوفر نموذج واحد مُنزّل لتوليد النصوص. في Anaconda Desktop، شغّل خادم نموذج. يوصى بشدة باستخدام نماذج تدعم استدعاء الأدوات.",
"provider.anaconda.state.noServer_other":
"تتوفر نماذج مُنزّلة لتوليد النصوص، وعددها {{count}}. في Anaconda Desktop، شغّل خادم نموذج. يوصى بشدة باستخدام نماذج تدعم استدعاء الأدوات.",
"command.category.suggested": "مقترح",
"command.category.view": "عرض",
"command.category.project": "مشروع",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Verificar novamente",
"provider.anaconda.state.noServer_one":
"Há 1 modelo de geração de texto baixado disponível. No Anaconda Desktop, inicie um servidor de modelo. Modelos com suporte a chamadas de ferramentas são altamente recomendados.",
"provider.anaconda.state.noServer_other":
"Há {{count}} modelos de geração de texto baixados disponíveis. No Anaconda Desktop, inicie um servidor de modelo. Modelos com suporte a chamadas de ferramentas são altamente recomendados.",
"command.category.suggested": "Sugerido",
"command.category.view": "Visualizar",
"command.category.project": "Projeto",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Provjeri ponovo",
"provider.anaconda.state.noServer_one":
"Dostupan je 1 preuzeti model za generisanje teksta. U Anaconda Desktopu pokrenite server modela. Modeli s podrškom za pozivanje alata se snažno preporučuju.",
"provider.anaconda.state.noServer_other":
"Preuzeti modeli za generisanje teksta dostupni su (ukupno: {{count}}). U Anaconda Desktopu pokrenite server modela. Modeli s podrškom za pozivanje alata se snažno preporučuju.",
"command.category.suggested": "Predloženo",
"command.category.view": "Prikaz",
"command.category.project": "Projekat",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Tjek igen",
"provider.anaconda.state.noServer_one":
"Der er 1 downloadet tekstgenereringsmodel tilgængelig. Start en modelserver i Anaconda Desktop. Modeller med understøttelse af værktøjskald anbefales kraftigt.",
"provider.anaconda.state.noServer_other":
"Der er {{count}} downloadede tekstgenereringsmodeller tilgængelige. Start en modelserver i Anaconda Desktop. Modeller med understøttelse af værktøjskald anbefales kraftigt.",
"command.category.suggested": "Foreslået",
"command.category.view": "Vis",
"command.category.project": "Projekt",
+7 -1
View File
@@ -1,8 +1,14 @@
import { dict as en } from "./en"
import { anacondaDesktopDict, dict as en } from "./en"
type Keys = keyof typeof en
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Erneut prüfen",
"provider.anaconda.state.noServer_one":
"1 heruntergeladenes Textgenerierungsmodell ist verfügbar. Starten Sie in Anaconda Desktop einen Modellserver. Modelle mit Unterstützung für Tool-Aufrufe werden dringend empfohlen.",
"provider.anaconda.state.noServer_other":
"{{count}} heruntergeladene Textgenerierungsmodelle sind verfügbar. Starten Sie in Anaconda Desktop einen Modellserver. Modelle mit Unterstützung für Tool-Aufrufe werden dringend empfohlen.",
"command.category.suggested": "Vorgeschlagen",
"command.category.view": "Ansicht",
"command.category.project": "Projekt",
@@ -1,4 +1,57 @@
export const anacondaDesktopDict = {
"provider.anaconda.title.connect": "Connect Anaconda Desktop",
"provider.anaconda.title.manage": "Manage Anaconda Desktop",
"provider.anaconda.status.checking": "Checking Anaconda Desktop...",
"provider.anaconda.status.opening": "Opening Anaconda Desktop...",
"provider.anaconda.status.syncing": "Refreshing provider models...",
"provider.anaconda.status.ready": "Ready to connect",
"provider.anaconda.status.waiting": "Waiting for Desktop",
"provider.anaconda.status.attention": "Needs attention",
"provider.anaconda.status.unavailable": "Unavailable",
"provider.anaconda.state.unsupported": "Anaconda Desktop is not supported on {{platform}}.",
"provider.anaconda.state.notInstalled":
"Install Anaconda Desktop on this machine, then return here. Kilo does not run the installer for you.",
"provider.anaconda.state.notRunning": "Open Anaconda Desktop, finish setup and sign in, then choose Check again.",
"provider.anaconda.state.invalidConfig":
"Anaconda Desktop setup is incomplete. Open Desktop, finish setup, and restart it if needed.",
"provider.anaconda.state.signedOut": "Open Anaconda Desktop and sign in before connecting Kilo.",
"provider.anaconda.state.unauthorized":
"Kilo could not access Anaconda Desktop. Open Desktop, sign in again, and restart it if needed.",
"provider.anaconda.state.unavailable":
"Anaconda Desktop is not responding yet. Open it and wait for the application to finish starting.",
"provider.anaconda.state.noModel":
"In Anaconda Desktop, download a text-generation model. Choose one with tool calling when possible, then start its server.",
"provider.anaconda.state.noServer_one":
"1 downloaded text-generation model is available. In Anaconda Desktop, start a model server. Models with tool calling support are strongly recommended.",
"provider.anaconda.state.noServer_other":
"{{count}} downloaded text-generation models are available. In Anaconda Desktop, start a model server. Models with tool calling support are strongly recommended.",
"provider.anaconda.state.unhealthy":
"The active inference server is not healthy yet. Check it in Anaconda Desktop and restart the server if needed.",
"provider.anaconda.state.ready":
"Kilo found a healthy local text-generation server and can import its current connection settings.",
"provider.anaconda.server": "Active inference server",
"provider.anaconda.context": "Context window",
"provider.anaconda.contextValue": "{{count}} tokens",
"provider.anaconda.tools": "Tool calling",
"provider.anaconda.tools.supported": "Supported",
"provider.anaconda.tools.unsupported": "Not enabled",
"provider.anaconda.tools.unknown": "Unknown",
"provider.anaconda.warning.title": "Tool support is limited",
"provider.anaconda.warning.description":
"This server does not confirm tool calling. Coding-agent actions may fail or be unavailable. Continue only if you accept these limitations.",
"provider.anaconda.action.download": "Download Anaconda Desktop",
"provider.anaconda.action.open": "Open Anaconda Desktop",
"provider.anaconda.action.checkAgain": "Check again",
"provider.anaconda.action.continue": "Continue anyway",
"provider.anaconda.action.manage": "Manage / Refresh",
"provider.anaconda.toast.refreshed.title": "Anaconda Desktop refreshed",
"provider.anaconda.toast.refreshed.description": "The active local server and models are up to date in Kilo.",
"settings.providers.note.anacondaDesktop": "Run a model served locally by Anaconda Desktop.",
"settings.providers.tag.local": "Local",
} as const
export const dict = {
...anacondaDesktopDict,
"command.category.suggested": "Suggested",
"command.category.view": "View",
"command.category.project": "Project",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Comprobar de nuevo",
"provider.anaconda.state.noServer_one":
"Hay 1 modelo de generación de texto descargado disponible. En Anaconda Desktop, inicia un servidor de modelos. Se recomienda encarecidamente usar modelos compatibles con llamadas a herramientas.",
"provider.anaconda.state.noServer_other":
"Hay {{count}} modelos de generación de texto descargados disponibles. En Anaconda Desktop, inicia un servidor de modelos. Se recomienda encarecidamente usar modelos compatibles con llamadas a herramientas.",
"command.category.suggested": "Sugerido",
"command.category.view": "Ver",
"command.category.project": "Proyecto",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Vérifier à nouveau",
"provider.anaconda.state.noServer_one":
"1 modèle de génération de texte téléchargé est disponible. Dans Anaconda Desktop, démarrez un serveur de modèle. Les modèles prenant en charge lappel doutils sont vivement recommandés.",
"provider.anaconda.state.noServer_other":
"{{count}} modèles de génération de texte téléchargés sont disponibles. Dans Anaconda Desktop, démarrez un serveur de modèle. Les modèles prenant en charge lappel doutils sont vivement recommandés.",
"command.category.suggested": "Suggéré",
"command.category.view": "Affichage",
"command.category.project": "Projet",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Controlla di nuovo",
"provider.anaconda.state.noServer_one":
"È disponibile 1 modello di generazione del testo scaricato. In Anaconda Desktop, avvia un server di modelli. I modelli che supportano le chiamate agli strumenti sono fortemente consigliati.",
"provider.anaconda.state.noServer_other":
"Sono disponibili {{count}} modelli di generazione del testo scaricati. In Anaconda Desktop, avvia un server di modelli. I modelli che supportano le chiamate agli strumenti sono fortemente consigliati.",
"command.category.suggested": "Suggeriti",
"command.category.view": "Vista",
"command.category.project": "Progetto",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "再確認",
"provider.anaconda.state.noServer_one":
"ダウンロード済みのテキスト生成モデルが1つ利用可能です。Anaconda Desktopでモデルサーバーを起動してください。ツール呼び出しに対応したモデルの使用を強く推奨します。",
"provider.anaconda.state.noServer_other":
"ダウンロード済みのテキスト生成モデルが{{count}}個利用可能です。Anaconda Desktopでモデルサーバーを起動してください。ツール呼び出しに対応したモデルの使用を強く推奨します。",
"command.category.suggested": "おすすめ",
"command.category.view": "表示",
"command.category.project": "プロジェクト",
+7 -1
View File
@@ -1,8 +1,14 @@
import { dict as en } from "./en"
import { anacondaDesktopDict, dict as en } from "./en"
type Keys = keyof typeof en
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "다시 확인",
"provider.anaconda.state.noServer_one":
"다운로드된 텍스트 생성 모델 1개를 사용할 수 있습니다. Anaconda Desktop에서 모델 서버를 시작하세요. 도구 호출을 지원하는 모델을 강력히 권장합니다.",
"provider.anaconda.state.noServer_other":
"다운로드된 텍스트 생성 모델 {{count}}개를 사용할 수 있습니다. Anaconda Desktop에서 모델 서버를 시작하세요. 도구 호출을 지원하는 모델을 강력히 권장합니다.",
"command.category.suggested": "추천",
"command.category.view": "보기",
"command.category.project": "프로젝트",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Opnieuw controleren",
"provider.anaconda.state.noServer_one":
"Er is 1 gedownload tekstgeneratiemodel beschikbaar. Start een modelserver in Anaconda Desktop. Modellen met ondersteuning voor toolaanroepen worden sterk aanbevolen.",
"provider.anaconda.state.noServer_other":
"Er zijn {{count}} gedownloade tekstgeneratiemodellen beschikbaar. Start een modelserver in Anaconda Desktop. Modellen met ondersteuning voor toolaanroepen worden sterk aanbevolen.",
"command.category.suggested": "Voorgesteld",
"command.category.view": "Weergave",
"command.category.project": "Project",
+7 -1
View File
@@ -1,7 +1,13 @@
import { dict as en } from "./en"
import { anacondaDesktopDict, dict as en } from "./en"
type Keys = keyof typeof en
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Sjekk igjen",
"provider.anaconda.state.noServer_one":
"1 nedlastet tekstgenereringsmodell er tilgjengelig. Start en modellserver i Anaconda Desktop. Modeller med støtte for verktøykall anbefales på det sterkeste.",
"provider.anaconda.state.noServer_other":
"{{count}} nedlastede tekstgenereringsmodeller er tilgjengelige. Start en modellserver i Anaconda Desktop. Modeller med støtte for verktøykall anbefales på det sterkeste.",
"command.category.suggested": "Foreslått",
"command.category.view": "Visning",
"command.category.project": "Prosjekt",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Sprawdź ponownie",
"provider.anaconda.state.noServer_one":
"Dostępny jest 1 pobrany model generowania tekstu. W Anaconda Desktop uruchom serwer modelu. Zdecydowanie zalecamy modele obsługujące wywoływanie narzędzi.",
"provider.anaconda.state.noServer_other":
"Dostępne są pobrane modele generowania tekstu (łącznie: {{count}}). W Anaconda Desktop uruchom serwer modelu. Zdecydowanie zalecamy modele obsługujące wywoływanie narzędzi.",
"command.category.suggested": "Sugerowane",
"command.category.view": "Widok",
"command.category.project": "Projekt",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Проверить снова",
"provider.anaconda.state.noServer_one":
"Доступна 1 загруженная модель генерации текста. Запустите сервер модели в Anaconda Desktop. Настоятельно рекомендуется использовать модели с поддержкой вызова инструментов.",
"provider.anaconda.state.noServer_other":
"Доступно загруженных моделей генерации текста: {{count}}. Запустите сервер модели в Anaconda Desktop. Настоятельно рекомендуется использовать модели с поддержкой вызова инструментов.",
"command.category.suggested": "Предложено",
"command.category.view": "Просмотр",
"command.category.project": "Проект",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "ตรวจสอบอีกครั้ง",
"provider.anaconda.state.noServer_one":
"มีโมเดลสร้างข้อความที่ดาวน์โหลดไว้แล้วพร้อมใช้งานอยู่ 1 โมเดล โปรดเริ่มเซิร์ฟเวอร์โมเดลใน Anaconda Desktop ขอแนะนำอย่างยิ่งให้ใช้โมเดลที่รองรับการเรียกใช้เครื่องมือ",
"provider.anaconda.state.noServer_other":
"มีโมเดลสร้างข้อความที่ดาวน์โหลดไว้แล้วพร้อมใช้งานอยู่ {{count}} โมเดล โปรดเริ่มเซิร์ฟเวอร์โมเดลใน Anaconda Desktop ขอแนะนำอย่างยิ่งให้ใช้โมเดลที่รองรับการเรียกใช้เครื่องมือ",
"command.category.suggested": "แนะนำ",
"command.category.view": "มุมมอง",
"command.category.project": "โปรเจกต์",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Tekrar kontrol et",
"provider.anaconda.state.noServer_one":
"İndirilmiş 1 metin üretme modeli kullanılabilir. Anaconda Desktop'ta bir model sunucusu başlatın. Araç çağırma desteği olan modeller önemle tavsiye edilir.",
"provider.anaconda.state.noServer_other":
"İndirilmiş {{count}} metin üretme modeli kullanılabilir. Anaconda Desktop'ta bir model sunucusu başlatın. Araç çağırma desteği olan modeller önemle tavsiye edilir.",
"command.category.suggested": "Önerilen",
"command.category.view": "Görünüm",
"command.category.project": "Proje",
+8
View File
@@ -1,4 +1,12 @@
import { anacondaDesktopDict } from "./en"
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "Перевірити ще раз",
"provider.anaconda.state.noServer_one":
"Доступна 1 завантажена модель генерації тексту. Запустіть сервер моделі в Anaconda Desktop. Наполегливо рекомендуємо використовувати моделі з підтримкою виклику інструментів.",
"provider.anaconda.state.noServer_other":
"Доступно завантажених моделей генерації тексту: {{count}}. Запустіть сервер моделі в Anaconda Desktop. Наполегливо рекомендуємо використовувати моделі з підтримкою виклику інструментів.",
"command.category.suggested": "Запропоновані",
"command.category.view": "Вигляд",
"command.category.project": "Проєкт",
+7 -1
View File
@@ -1,8 +1,14 @@
import { dict as en } from "./en"
import { anacondaDesktopDict, dict as en } from "./en"
type Keys = keyof typeof en
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "再次检查",
"provider.anaconda.state.noServer_one":
"有 1 个已下载的文本生成模型可用。请在 Anaconda Desktop 中启动一个模型服务器。强烈建议使用支持工具调用的模型。",
"provider.anaconda.state.noServer_other":
"有 {{count}} 个已下载的文本生成模型可用。请在 Anaconda Desktop 中启动一个模型服务器。强烈建议使用支持工具调用的模型。",
"command.category.suggested": "建议",
"command.category.view": "视图",
"command.category.project": "项目",
+7 -1
View File
@@ -1,8 +1,14 @@
import { dict as en } from "./en"
import { anacondaDesktopDict, dict as en } from "./en"
type Keys = keyof typeof en
export const dict = {
...anacondaDesktopDict,
"provider.anaconda.action.checkAgain": "再次檢查",
"provider.anaconda.state.noServer_one":
"有 1 個已下載的文字生成模型可用。請在 Anaconda Desktop 中啟動一個模型伺服器。強烈建議使用支援工具呼叫的模型。",
"provider.anaconda.state.noServer_other":
"有 {{count}} 個已下載的文字生成模型可用。請在 Anaconda Desktop 中啟動一個模型伺服器。強烈建議使用支援工具呼叫的模型。",
"command.category.suggested": "建議",
"command.category.view": "檢視",
"command.category.project": "專案",
@@ -0,0 +1,76 @@
/** @jsxImportSource solid-js */
/** Stories for Anaconda Desktop provider setup. */
import type { Meta, StoryObj } from "storybook-solidjs-vite"
import type { AnacondaDesktopStatus } from "@kilocode/sdk/v2/client"
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
import { onMount } from "solid-js"
import { StoryProviders } from "./StoryProviders"
import AnacondaDesktopDialog from "../components/settings/AnacondaDesktopDialog"
const meta: Meta = {
title: "Anaconda Desktop",
parameters: { layout: "fullscreen" },
}
export default meta
type Story = StoryObj
function Dialog(props: { status: AnacondaDesktopStatus }) {
const dialog = useDialog()
onMount(() => dialog.show(() => <AnacondaDesktopDialog status={props.status} />))
return null
}
export const NotInstalled: Story = {
name: "Not installed",
render: () => (
<StoryProviders>
<Dialog status={{ type: "not-installed", downloadURL: "about:blank" }} />
</StoryProviders>
),
}
export const Waiting: Story = {
name: "Waiting for server",
render: () => (
<StoryProviders>
<Dialog status={{ type: "no-running-server", downloadedModels: 3 }} />
</StoryProviders>
),
}
export const Ready: Story = {
name: "Ready with tools",
render: () => (
<StoryProviders>
<Dialog
status={{
type: "ready",
serverID: "local-server",
serverName: "Qwen Coder",
models: [{ id: "qwen-coder", name: "Qwen 2.5 Coder 14B" }],
context: 32768,
toolcall: "supported",
}}
/>
</StoryProviders>
),
}
export const LimitedTools: Story = {
name: "Ready without tools",
render: () => (
<StoryProviders>
<Dialog
status={{
type: "ready",
serverID: "local-server",
serverName: "Llama Local",
models: [{ id: "llama-local", name: "Llama 3.2 3B Instruct" }],
context: 16384,
toolcall: "unsupported",
}}
/>
</StoryProviders>
),
}
@@ -14,6 +14,7 @@ import type {
SessionUpdate,
} from "./sessions"
import type { PermissionRequest } from "./permissions"
import type { AnacondaDesktopExtensionMessage } from "../../../../src/anaconda-desktop/messages"
import type { QuestionRequest, SuggestionRequest, TodoItem } from "./questions"
import type { ModelSelection, Provider, ProviderAuthState } from "./providers"
import type { AgentInfo, SkillInfo, SlashCommandInfo } from "./agents"
@@ -1125,6 +1126,7 @@ export type ExtensionMessage =
| ProviderConnectedMessage
| ProviderDisconnectedMessage
| ProviderActionErrorMessage
| AnacondaDesktopExtensionMessage
| CustomProviderModelsFetchedMessage
| RecentsLoadedMessage
| FavoritesLoadedMessage
@@ -7,6 +7,7 @@ import type { Config } from "./config"
import type { ModelAllocation, ReviewComment } from "./agent-manager"
import type { ReviewMessageData } from "../../../../src/shared/review-comments"
import type { WorkStyle, WorkStyleState } from "../../../../src/shared/work-style-presets"
import type { AnacondaDesktopWebviewMessage } from "../../../../src/anaconda-desktop/messages"
import type {
ClearLegacyDataMessage,
FinalizeLegacyMigrationMessage,
@@ -1282,6 +1283,7 @@ export type WebviewMessage =
| AuthorizeProviderOAuthMessage
| CompleteProviderOAuthMessage
| DisconnectProviderMessage
| AnacondaDesktopWebviewMessage
| SaveCustomProviderMessage
| FetchCustomProviderModelsMessage
| PersistRecentsRequest
@@ -0,0 +1,57 @@
import type { ExtensionMessage, WebviewMessage } from "../types/messages"
import type {
AnacondaDesktopError,
AnacondaDesktopRequest,
AnacondaDesktopResult,
} from "../../../src/anaconda-desktop/messages"
type Transport = {
postMessage: (message: WebviewMessage) => void
onMessage: (handler: (message: ExtensionMessage) => void) => () => void
}
type RequestInput<T> = T extends { requestId: string } ? Omit<T, "requestId"> : never
type Handlers = {
onStatus?: (message: Extract<AnacondaDesktopResult, { type: "anacondaDesktopStatusResult" }>) => void
onOpened?: (message: Extract<AnacondaDesktopResult, { type: "anacondaDesktopOpened" }>) => void
onSynced?: (message: Extract<AnacondaDesktopResult, { type: "anacondaDesktopSynced" }>) => void
onError?: (message: AnacondaDesktopError) => void
}
export function createAnacondaDesktopAction(vscode: Transport) {
const pending = new Map<string, Handlers>()
const unsubscribe = vscode.onMessage((message) => {
if (!message.type.startsWith("anacondaDesktop") || !("requestId" in message)) return
const handlers = pending.get(message.requestId)
if (!handlers) return
pending.delete(message.requestId)
if (message.type === "anacondaDesktopStatusResult") handlers.onStatus?.(message)
if (message.type === "anacondaDesktopOpened") handlers.onOpened?.(message)
if (message.type === "anacondaDesktopSynced") handlers.onSynced?.(message)
if (message.type === "anacondaDesktopActionError") handlers.onError?.(message)
})
function send(message: RequestInput<AnacondaDesktopRequest>, handlers: Handlers = {}) {
const requestId = crypto.randomUUID()
pending.set(requestId, handlers)
vscode.postMessage({ ...message, requestId } as AnacondaDesktopRequest)
return requestId
}
function clear(requestId?: string) {
const ids = requestId ? [requestId] : [...pending.keys()]
for (const id of ids) {
if (!pending.delete(id)) continue
vscode.postMessage({ type: "cancelAnacondaDesktopRequest", requestId: id })
}
}
function dispose() {
clear()
unsubscribe()
}
return { clear, send, dispose }
}
@@ -135,6 +135,7 @@ export function createDialogProviderOptions() {
gutter: failedGutter ?? (connected && onboarded() ? () => <text fg={theme.success}></text> : undefined), // kilocode_change
async onSelect() {
if (consoleManaged) return
if (KiloProvider.selectProvider({ providerID, replace: dialog.replace, model: DialogModel })) return // kilocode_change
const methods = sync.data.provider_auth[providerID] ?? [
{
@@ -0,0 +1,413 @@
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Context, Duration, Effect, Layer, Option, Redacted, Result, Schema } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
import * as DesktopPlatform from "./platform"
import {
endpoint,
normalizeLoopbackEndpoint,
readConfig,
readStore,
warning,
CatalogResponse,
DOWNLOAD_URL,
HealthResponse,
InferenceModelsResponse,
ManagementRoot,
PropsResponse,
REQUEST_TIMEOUT,
RequestError,
ServersResponse,
type CatalogModel,
type InferenceModel,
type Metadata,
type ModelDescriptor,
type PropsResponse as Props,
type Server,
type Status,
type ToolCapability,
} from "./domain"
export interface Connection {
readonly key: Redacted.Redacted<string>
readonly metadata: Metadata
}
export interface DiscoveryResult {
readonly status: Status
readonly connection?: Connection
}
export interface Interface {
readonly discover: () => Effect.Effect<DiscoveryResult>
}
export class Service extends Context.Service<Service, Interface>()("@kilocode/AnacondaDesktopDiscovery") {}
interface Options {
readonly timeout?: Duration.Input
}
function record(input: unknown): input is Readonly<Record<string, unknown>> {
return typeof input === "object" && input !== null && !Array.isArray(input)
}
function secret(input: Redacted.Redacted<string> | undefined) {
return input ?? Redacted.make("", { label: "Anaconda inference key" })
}
const decodeRoot = Schema.decodeUnknownOption(ManagementRoot)
const decodeCatalog = Schema.decodeUnknownOption(CatalogResponse)
const decodeServers = Schema.decodeUnknownOption(ServersResponse)
const decodeHealth = Schema.decodeUnknownOption(HealthResponse)
const decodeModels = Schema.decodeUnknownOption(InferenceModelsResponse)
const decodeProps = Schema.decodeUnknownOption(PropsResponse)
function request(
http: HttpClient.HttpClient,
target: "management" | "inference",
url: string,
key: Redacted.Redacted<string>,
timeout: Duration.Input,
) {
const base = HttpClientRequest.get(url).pipe(HttpClientRequest.acceptJson)
const auth = Redacted.value(key) === "" ? base : base.pipe(HttpClientRequest.bearerToken(key))
return http.execute(auth).pipe(
Effect.flatMap((response) => {
if (response.status === 401 || response.status === 403) {
return Effect.fail(new RequestError({ target, reason: "unauthorized" }))
}
if (response.status < 200 || response.status >= 300) {
return Effect.fail(new RequestError({ target, reason: "unexpected-status" }))
}
return response.json.pipe(Effect.mapError(() => new RequestError({ target, reason: "malformed" })))
}),
Effect.timeoutOrElse({
duration: timeout,
orElse: () => Effect.fail(new RequestError({ target, reason: "timeout" })),
}),
Effect.mapError((error) =>
error instanceof RequestError ? error : new RequestError({ target, reason: "transport" }),
),
)
}
function management(error: RequestError): DiscoveryResult {
if (error.reason === "transport") return { status: { type: "not-running" } }
if (error.reason === "unauthorized") return { status: { type: "management-unauthorized" } }
return {
status: {
type: "management-unavailable",
reason: error.reason === "timeout" ? "timeout" : "unexpected-response",
},
}
}
function unhealthy(serverID: string): DiscoveryResult {
return { status: { type: "inference-unhealthy", serverID } }
}
function identity(server: Server) {
if (server.id) return server.id
if (typeof server.serverProcessId === "number") return `process-${server.serverProcessId}`
return server.modelFile?.id ?? "unknown"
}
function running(server: Server) {
if (server.status.trim().toLowerCase() !== "running") return false
return !server.tag || server.tag.trim().toLowerCase() === "inference"
}
function params(server: Server) {
return server.server ?? server.serverConfig?.serverParams ?? server.serverConfig?.apiParams ?? server
}
function key(server: Server) {
const runtime = server.server
const config = server.serverConfig
return secret(
runtime?.api_key ??
runtime?.apiKey ??
config?.serverParams?.api_key ??
config?.serverParams?.apiKey ??
config?.apiParams?.api_key ??
config?.apiParams?.apiKey ??
config?.api_key ??
config?.apiKey ??
server.api_key ??
server.apiKey,
)
}
function location(server: Server) {
const source = params(server)
const url = source.url ?? server.url
if (url) return normalizeLoopbackEndpoint(url)
const hostname = source.host ?? server.host
const port = source.port ?? server.port
if (!hostname || typeof port !== "number" || !Number.isInteger(port)) return
return endpoint(hostname, port)
}
function files(model: CatalogModel) {
return [
...(model.files ?? []),
...(model.metadata?.files ?? []),
...(model.metadata?.quantizations ?? []).map((item) => ({ name: item.modelFileName })),
]
}
function catalog(models: ReadonlyArray<CatalogModel>, server: Server) {
const file = server.modelFile
if (!file) return
return models.find((model) => {
if (file.id && files(model).some((item) => "id" in item && item.id === file.id)) return true
if (file.name && files(model).some((item) => item.name === file.name)) return true
return file.name === model.name
})
}
function generation(model: CatalogModel) {
const task = (model.trainedFor ?? model.metadata?.trainedFor)?.trim().toLowerCase()
return !task || task === "text-generation"
}
function text(model: CatalogModel | undefined, props: Props) {
if (model && !generation(model)) return false
const kind = [props.type, props.model_type, props.task]
.filter((item): item is string => typeof item === "string")
.join(" ")
.toLowerCase()
if (/embed|rerank|sentence[-_ ]similarity/.test(kind)) return false
const capabilities = record(props.capabilities) ? props.capabilities : undefined
if (capabilities?.chat === false || capabilities?.completion === false) return false
return true
}
function size(props: Props, model: CatalogModel | undefined) {
const defaults = record(props.default_generation_settings) ? props.default_generation_settings : undefined
const values = [
defaults?.n_ctx,
props.n_ctx,
props.context_size,
props.contextWindowSize,
model?.contextWindowSize,
model?.metadata?.contextWindowSize,
]
const found = values.find((item) => typeof item === "number" && Number.isFinite(item) && item >= 0)
return typeof found === "number" ? Math.floor(found) : 0
}
function tools(props: Props): ToolCapability {
const caps = record(props.chat_template_caps) ? props.chat_template_caps : undefined
if (!caps) return "unknown"
const values = [caps.supports_tools, caps.supports_tool_calls, caps.supportsToolCalls].filter(
(item): item is boolean => typeof item === "boolean",
)
if (values.includes(true)) return "supported"
if (values.length > 0) return "unsupported"
return "unknown"
}
type Modality = "text" | "audio" | "image" | "video" | "pdf"
function modality(input: string): input is Modality {
if (input === "text") return true
if (input === "audio") return true
if (input === "image") return true
if (input === "video") return true
return input === "pdf"
}
function list(input: ReadonlyArray<string> | undefined) {
return (input ?? []).map((item) => item.toLowerCase()).filter(modality)
}
function descriptors(
entries: ReadonlyArray<InferenceModel>,
model: CatalogModel | undefined,
server: Server,
props: Props,
toolcall: ToolCapability,
): ModelDescriptor[] {
const reported = record(props.modalities) ? props.modalities : undefined
const vision = reported?.vision === true
const audio = reported?.audio === true
const file = server.modelFile?.name
const base = model?.name ?? file
const note = warning(toolcall)
return entries.map((entry) => {
const input = new Set<Modality>(["text", ...list(entry.modalities?.input)])
const output = new Set<Modality>(["text", ...list(entry.modalities?.output)])
if (vision) input.add("image")
if (audio) input.add("audio")
const name = entries.length === 1 && base ? base : entry.id
const family =
model?.family ?? model?.metadata?.family ?? model?.model_type ?? model?.metadata?.model_type ?? entry.family
return {
id: entry.id,
name,
...(family?.trim() ? { family: family.trim() } : {}),
input: [...input],
output: [...output],
...(note ? { description: note } : {}),
}
})
}
export function makeLayer(options: Options = {}) {
return Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const http = yield* HttpClient.HttpClient
const platform = yield* DesktopPlatform.Service
const timeout = options.timeout ?? REQUEST_TIMEOUT
const discover = Effect.fn("AnacondaDesktopDiscovery.discover")(function* () {
if (!DesktopPlatform.supported(platform.info)) {
return {
status: { type: "unsupported-platform", platform: platform.info.platform },
} satisfies DiscoveryResult
}
const install = yield* platform.installation().pipe(Effect.orElseSucceed(() => undefined))
if (!install) {
return { status: { type: "not-installed", downloadURL: DOWNLOAD_URL } } satisfies DiscoveryResult
}
const dir = yield* platform.dataDir().pipe(Effect.orElseSucceed(() => undefined))
if (!dir) {
return {
status: { type: "unsupported-platform", platform: platform.info.platform },
} satisfies DiscoveryResult
}
const cfg = yield* readConfig(dir).pipe(Effect.provideService(AppFileSystem.Service, fs), Effect.result)
if (Result.isFailure(cfg)) {
return {
status: { type: "invalid-config", reason: cfg.failure.reason },
} satisfies DiscoveryResult
}
const origin = `http://127.0.0.1:${cfg.success.aiNavApiServerPort}`
const root = yield* request(http, "management", `${origin}/api`, cfg.success.aiNavApiKey, timeout).pipe(
Effect.result,
)
if (Result.isFailure(root)) return management(root.failure)
if (Option.isNone(decodeRoot(root.success))) {
return management(new RequestError({ target: "management", reason: "malformed" }))
}
const signed = yield* readStore(dir).pipe(Effect.provideService(AppFileSystem.Service, fs), Effect.result)
if (Result.isFailure(signed) || !signed.success) {
return { status: { type: "signed-out" } } satisfies DiscoveryResult
}
const found = yield* request(
http,
"management",
`${origin}/api/models?downloaded=true`,
cfg.success.aiNavApiKey,
timeout,
).pipe(Effect.result)
if (Result.isFailure(found)) return management(found.failure)
const decodedModels = decodeCatalog(found.success)
if (Option.isNone(decodedModels)) {
return management(new RequestError({ target: "management", reason: "malformed" }))
}
const downloaded = decodedModels.value.data.filter((model) => files(model).length > 0 && generation(model))
if (downloaded.length === 0) {
return { status: { type: "no-downloaded-model" } } satisfies DiscoveryResult
}
const queried = yield* request(
http,
"management",
`${origin}/api/servers?status=running&tag=inference`,
cfg.success.aiNavApiKey,
timeout,
).pipe(Effect.result)
if (Result.isFailure(queried)) return management(queried.failure)
const decodedServers = decodeServers(queried.success)
if (Option.isNone(decodedServers)) {
return management(new RequestError({ target: "management", reason: "malformed" }))
}
const servers = decodedServers.value.data.filter(running)
if (servers.length === 0) {
return {
status: {
type: "no-running-server",
downloadedModels: downloaded.length,
},
} satisfies DiscoveryResult
}
const server = servers[0]
const serverID = identity(server)
const baseURL = location(server)
if (!baseURL) return unhealthy(serverID)
const api = baseURL.replace(/\/v1$/, "")
const inferenceKey = key(server)
const health = yield* request(http, "inference", `${api}/health`, inferenceKey, timeout).pipe(Effect.result)
if (Result.isFailure(health)) return unhealthy(serverID)
const decodedHealth = decodeHealth(health.success)
if (Option.isNone(decodedHealth) || !["ok", "healthy"].includes(decodedHealth.value.status.toLowerCase())) {
return unhealthy(serverID)
}
const listed = yield* request(http, "inference", `${api}/v1/models`, inferenceKey, timeout).pipe(Effect.result)
if (Result.isFailure(listed)) return unhealthy(serverID)
const decodedInference = decodeModels(listed.success)
if (Option.isNone(decodedInference) || decodedInference.value.data.length === 0) return unhealthy(serverID)
const properties = yield* request(http, "inference", `${api}/props`, inferenceKey, timeout).pipe(Effect.result)
if (Result.isFailure(properties)) return unhealthy(serverID)
const decodedProps = decodeProps(properties.success)
if (Option.isNone(decodedProps)) return unhealthy(serverID)
const model = catalog(downloaded, server)
if (!text(model, decodedProps.value)) return unhealthy(serverID)
const context = size(decodedProps.value, model)
const toolcall = tools(decodedProps.value)
const models = descriptors(decodedInference.value.data, model, server, decodedProps.value, toolcall)
const metadata: Metadata = {
version: "1",
serverID,
baseURL,
models,
context,
toolcall,
}
return {
status: {
type: "ready",
serverID,
...(model?.name
? { serverName: model.name }
: server.modelFile?.name
? { serverName: server.modelFile.name }
: {}),
models: models.map((item) => ({
id: item.id,
name: item.name,
})),
context,
toolcall,
},
connection: { key: inferenceKey, metadata },
} satisfies DiscoveryResult
})
return Service.of({ discover })
}),
)
}
export const layer = makeLayer()
export const defaultLayer = layer.pipe(
Layer.provide(DesktopPlatform.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(FetchHttpClient.layer),
)
@@ -0,0 +1,429 @@
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import path from "path"
import { Effect, Option, Schema } from "effect"
export const PROVIDER_ID = "anaconda-desktop"
export const DOWNLOAD_URL = "https://www.anaconda.com/products/desktop"
export const CONFIG_FILE = "config.json"
export const STORE_FILE = "anaconda-desktop-encrypted-store.json"
export const OAUTH_SUFFIX = "_ai-navigator-workos-oauth"
export const REQUEST_TIMEOUT = "4 seconds"
export const ToolCapability = Schema.Literals(["supported", "unsupported", "unknown"])
export type ToolCapability = typeof ToolCapability.Type
export const Modality = Schema.Literals(["text", "audio", "image", "video", "pdf"])
export type Modality = typeof Modality.Type
export const Port = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65_535 }))
export const ContextSize = Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }))
export const ModelDescriptor = Schema.Struct({
id: Schema.NonEmptyString,
name: Schema.NonEmptyString,
family: Schema.optional(Schema.NonEmptyString),
input: Schema.Array(Modality).check(Schema.isNonEmpty()),
output: Schema.Array(Modality).check(Schema.isNonEmpty()),
description: Schema.optional(Schema.NonEmptyString),
})
export type ModelDescriptor = typeof ModelDescriptor.Type
export const Metadata = Schema.Struct({
version: Schema.Literal("1"),
serverID: Schema.NonEmptyString,
baseURL: Schema.NonEmptyString,
models: Schema.Array(ModelDescriptor).check(Schema.isNonEmpty()),
context: ContextSize,
toolcall: ToolCapability,
})
export type Metadata = typeof Metadata.Type
export const EncodedMetadata = Schema.Struct({
version: Schema.Literal("1"),
serverID: Schema.NonEmptyString,
baseURL: Schema.NonEmptyString,
models: Schema.fromJsonString(Schema.Array(ModelDescriptor).check(Schema.isNonEmpty())),
context: Schema.NumberFromString.pipe(
Schema.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER })),
),
toolcall: ToolCapability,
})
export const Config = Schema.Struct({
aiNavApiKey: Schema.RedactedFromValue(Schema.NonEmptyString, { label: "Anaconda Desktop management key" }),
aiNavApiServerPort: Port,
})
export type Config = typeof Config.Type
const CatalogFile = Schema.Struct({
id: Schema.optional(Schema.String),
name: Schema.optional(Schema.String),
})
const CatalogQuantization = Schema.Struct({
modelFileName: Schema.optional(Schema.String),
})
const CatalogMetadata = Schema.Struct({
trainedFor: Schema.optional(Schema.String),
contextWindowSize: Schema.optional(Schema.Finite),
description: Schema.optional(Schema.String),
model_type: Schema.optional(Schema.String),
family: Schema.optional(Schema.String),
quantizations: Schema.optional(Schema.Array(CatalogQuantization)),
files: Schema.optional(Schema.Array(CatalogFile)),
})
export const CatalogModel = Schema.Struct({
id: Schema.NonEmptyString,
name: Schema.NonEmptyString,
trainedFor: Schema.optional(Schema.String),
contextWindowSize: Schema.optional(Schema.Finite),
model_type: Schema.optional(Schema.String),
family: Schema.optional(Schema.String),
metadata: Schema.optional(CatalogMetadata),
files: Schema.optional(Schema.Array(CatalogFile)),
})
export type CatalogModel = typeof CatalogModel.Type
export const CatalogResponse = Schema.Struct({
data: Schema.Array(CatalogModel),
})
const KeyFields = {
apiKey: Schema.optional(Schema.RedactedFromValue(Schema.String, { label: "Anaconda inference key" })),
api_key: Schema.optional(Schema.RedactedFromValue(Schema.String, { label: "Anaconda inference key" })),
}
const Params = Schema.Struct({
host: Schema.optional(Schema.String),
port: Schema.optional(Schema.Finite),
url: Schema.optional(Schema.String),
...KeyFields,
})
const ServerConfig = Schema.Struct({
modelFileName: Schema.optional(Schema.String),
apiParams: Schema.optional(Params),
serverParams: Schema.optional(Params),
...KeyFields,
})
const ServerRuntime = Schema.Struct({
host: Schema.optional(Schema.String),
port: Schema.optional(Schema.Finite),
url: Schema.optional(Schema.String),
...KeyFields,
})
export const Server = Schema.Struct({
id: Schema.optional(Schema.NonEmptyString),
serverProcessId: Schema.optional(Schema.NullOr(Schema.Int)),
status: Schema.String,
tag: Schema.optional(Schema.String),
modelFile: Schema.optional(CatalogFile),
serverConfig: Schema.optional(ServerConfig),
server: Schema.optional(ServerRuntime),
host: Schema.optional(Schema.String),
port: Schema.optional(Schema.Finite),
url: Schema.optional(Schema.String),
...KeyFields,
})
export type Server = typeof Server.Type
export const ServersResponse = Schema.Struct({
data: Schema.Array(Server),
})
export const ManagementRoot = Schema.Struct({
data: Schema.Record(Schema.String, Schema.Unknown),
})
export const HealthResponse = Schema.Struct({
status: Schema.String,
})
const ReportedModalities = Schema.Struct({
input: Schema.optional(Schema.Array(Schema.String)),
output: Schema.optional(Schema.Array(Schema.String)),
})
export const InferenceModel = Schema.Struct({
id: Schema.NonEmptyString,
owned_by: Schema.optional(Schema.String),
family: Schema.optional(Schema.String),
modalities: Schema.optional(ReportedModalities),
})
export type InferenceModel = typeof InferenceModel.Type
export const InferenceModelsResponse = Schema.Struct({
data: Schema.Array(InferenceModel),
})
export const PropsResponse = Schema.Record(Schema.String, Schema.Unknown)
export type PropsResponse = typeof PropsResponse.Type
export const StatusModel = Schema.Struct({
id: Schema.NonEmptyString,
name: Schema.NonEmptyString,
})
export type StatusModel = typeof StatusModel.Type
export const UnsupportedStatus = Schema.Struct({
type: Schema.Literal("unsupported-platform"),
platform: Schema.String,
})
export const NotInstalledStatus = Schema.Struct({
type: Schema.Literal("not-installed"),
downloadURL: Schema.String,
})
export const NotRunningStatus = Schema.Struct({
type: Schema.Literal("not-running"),
})
export const InvalidConfigStatus = Schema.Struct({
type: Schema.Literal("invalid-config"),
reason: Schema.Literals(["missing", "malformed", "missing-key", "invalid-port"]),
})
export const SignedOutStatus = Schema.Struct({
type: Schema.Literal("signed-out"),
})
export const ManagementUnauthorizedStatus = Schema.Struct({
type: Schema.Literal("management-unauthorized"),
})
export const ManagementUnavailableStatus = Schema.Struct({
type: Schema.Literal("management-unavailable"),
reason: Schema.Literals(["timeout", "unexpected-response"]),
})
export const NoDownloadedModelStatus = Schema.Struct({
type: Schema.Literal("no-downloaded-model"),
})
export const NoRunningServerStatus = Schema.Struct({
type: Schema.Literal("no-running-server"),
downloadedModels: Schema.Int,
})
export const InferenceUnhealthyStatus = Schema.Struct({
type: Schema.Literal("inference-unhealthy"),
serverID: Schema.NonEmptyString,
})
export const ReadyStatus = Schema.Struct({
type: Schema.Literal("ready"),
serverID: Schema.NonEmptyString,
serverName: Schema.optional(Schema.NonEmptyString),
models: Schema.Array(StatusModel).check(Schema.isNonEmpty()),
context: ContextSize,
toolcall: ToolCapability,
})
export type ReadyStatus = typeof ReadyStatus.Type
export const Status = Schema.Union([
UnsupportedStatus,
NotInstalledStatus,
NotRunningStatus,
InvalidConfigStatus,
SignedOutStatus,
ManagementUnauthorizedStatus,
ManagementUnavailableStatus,
NoDownloadedModelStatus,
NoRunningServerStatus,
InferenceUnhealthyStatus,
ReadyStatus,
]).annotate({ discriminator: "type", identifier: "AnacondaDesktopStatus" })
export type Status = typeof Status.Type
export class ConfigError extends Schema.TaggedErrorClass<ConfigError>()("AnacondaDesktopConfigError", {
reason: Schema.Literals(["missing", "malformed", "missing-key", "invalid-port"]),
}) {}
export class StoreError extends Schema.TaggedErrorClass<StoreError>()("AnacondaDesktopStoreError", {
reason: Schema.Literals(["missing", "malformed"]),
}) {}
export class PlatformError extends Schema.TaggedErrorClass<PlatformError>()("AnacondaDesktopPlatformError", {
operation: Schema.Literals(["data-dir", "installation", "open"]),
reason: Schema.Literals(["unsupported", "not-installed", "failed"]),
}) {}
export class RequestError extends Schema.TaggedErrorClass<RequestError>()("AnacondaDesktopRequestError", {
target: Schema.Literals(["management", "inference"]),
reason: Schema.Literals(["timeout", "transport", "unauthorized", "unexpected-status", "malformed"]),
}) {}
export class NotReadyError extends Schema.TaggedErrorClass<NotReadyError>()("AnacondaDesktopNotReadyError", {
status: Status,
}) {}
export class ToolAcknowledgementError extends Schema.TaggedErrorClass<ToolAcknowledgementError>()(
"AnacondaDesktopToolAcknowledgementError",
{
status: ReadyStatus,
},
) {}
export class SyncError extends Schema.TaggedErrorClass<SyncError>()("AnacondaDesktopSyncError", {
operation: Schema.Literals(["encode", "store"]),
}) {}
const json = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
const config = Schema.decodeUnknownOption(Config)
function record(input: unknown): input is Record<string, unknown> {
return typeof input === "object" && input !== null && !Array.isArray(input)
}
export const parseConfig = Effect.fn("AnacondaDesktop.parseConfig")(function* (text: string) {
const parsed = json(text)
if (Option.isNone(parsed) || !record(parsed.value)) {
return yield* new ConfigError({ reason: "malformed" })
}
const key = parsed.value.aiNavApiKey
if (typeof key !== "string" || key.trim() === "") {
return yield* new ConfigError({ reason: "missing-key" })
}
const port = parsed.value.aiNavApiServerPort
if (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65_535) {
return yield* new ConfigError({ reason: "invalid-port" })
}
const result = config({ aiNavApiKey: key.trim(), aiNavApiServerPort: port })
if (Option.isNone(result)) return yield* new ConfigError({ reason: "malformed" })
return result.value
})
function present(input: unknown) {
if (input === null || input === undefined) return false
if (typeof input === "string") return input.trim() !== ""
if (Array.isArray(input)) return input.length > 0
if (record(input)) return Object.keys(input).length > 0
return true
}
export const parseStore = Effect.fn("AnacondaDesktop.parseStore")(function* (text: string) {
const parsed = json(text)
if (Option.isNone(parsed) || !record(parsed.value)) {
return yield* new StoreError({ reason: "malformed" })
}
return Object.entries(parsed.value).some(([key, value]) => key.endsWith(OAUTH_SUFFIX) && present(value))
})
export const readConfig = Effect.fn("AnacondaDesktop.readConfig")(function* (dir: string) {
const fs = yield* AppFileSystem.Service
const text = yield* fs
.readFileStringSafe(path.join(dir, CONFIG_FILE))
.pipe(Effect.mapError(() => new ConfigError({ reason: "malformed" as const })))
if (text === undefined) return yield* new ConfigError({ reason: "missing" })
return yield* parseConfig(text)
})
export const readStore = Effect.fn("AnacondaDesktop.readStore")(function* (dir: string) {
const fs = yield* AppFileSystem.Service
const text = yield* fs
.readFileStringSafe(path.join(dir, STORE_FILE))
.pipe(Effect.mapError(() => new StoreError({ reason: "malformed" as const })))
if (text === undefined) return yield* new StoreError({ reason: "missing" })
return yield* parseStore(text)
})
function host(value: string) {
const trimmed = value.trim().toLowerCase()
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed.slice(1, -1)
return trimmed
}
export function isLoopbackHost(value: string) {
const name = host(value)
if (name === "localhost" || name === "::1") return true
if (name.startsWith("::ffff:")) return isLoopbackHost(name.slice("::ffff:".length))
const octets = name.split(".")
if (octets.length !== 4) return false
if (octets.some((part) => !/^\d{1,3}$/.test(part) || Number(part) > 255)) return false
return Number(octets[0]) === 127
}
function clientHost(value: string) {
const name = host(value)
if (name === "0.0.0.0") return "127.0.0.1"
if (name === "::") return "::1"
return name
}
function parsed(input: string) {
const value = input.trim()
if (!URL.canParse(value)) return
const url = new URL(value)
if (url.protocol !== "http:") return
if (url.username || url.password || url.search || url.hash) return
if (url.pathname !== "/" && url.pathname !== "" && url.pathname !== "/v1" && url.pathname !== "/v1/") return
const name = clientHost(url.hostname)
if (!isLoopbackHost(name)) return
const fallback = url.port || "80"
const port = Number(fallback)
if (!Number.isInteger(port) || port < 1 || port > 65_535) return
const hostname = name.includes(":") ? `[${name}]` : name
return `http://${hostname}:${port}/v1`
}
export function normalizeLoopbackEndpoint(input: string) {
return parsed(input)
}
export function endpoint(hostname: string, port: number) {
if (!Number.isInteger(port) || port < 1 || port > 65_535) return
const value = hostname.trim()
if (!value) return
if (value.startsWith("http://") || value.startsWith("https://")) {
if (!URL.canParse(value)) return
const url = new URL(value)
if (!url.port) url.port = String(port)
return normalizeLoopbackEndpoint(url.toString())
}
const name = host(value)
const address = name.includes(":") ? `[${name}]` : name
return normalizeLoopbackEndpoint(`http://${address}:${port}`)
}
function unique(models: ReadonlyArray<ModelDescriptor>) {
return new Set(models.map((model) => model.id)).size === models.length
}
const decode = Schema.decodeUnknownOption(EncodedMetadata)
const encode = Schema.encodeUnknownOption(EncodedMetadata)
export function decodeMetadata(input: Record<string, string> | undefined): Metadata | undefined {
if (!input) return
const result = decode(input)
if (Option.isNone(result)) return
const baseURL = normalizeLoopbackEndpoint(result.value.baseURL)
if (!baseURL || !unique(result.value.models)) return
return { ...result.value, baseURL }
}
export function encodeMetadata(input: Metadata): Record<string, string> | undefined {
const checked = Schema.decodeUnknownOption(Metadata)(input)
if (Option.isNone(checked)) return
const baseURL = normalizeLoopbackEndpoint(checked.value.baseURL)
if (!baseURL || !unique(checked.value.models)) return
const result = encode({ ...checked.value, baseURL })
if (Option.isNone(result)) return
return result.value
}
export function warning(toolcall: ToolCapability) {
if (toolcall === "supported") return
if (toolcall === "unsupported") {
return "This local model does not support tool calling, so normal coding-agent actions are limited."
}
return "Tool-call support could not be confirmed for this local model, so normal coding-agent actions may be limited."
}
@@ -0,0 +1,207 @@
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Process } from "@/util/process"
import { arch, homedir } from "node:os"
import path from "path"
import { Context, Effect, Layer } from "effect"
import { PlatformError } from "./domain"
export interface Info {
readonly platform: NodeJS.Platform
readonly arch: string
readonly home: string
readonly env: NodeJS.ProcessEnv
}
export interface Installation {
readonly path: string
}
export interface Interface {
readonly info: Info
readonly dataDir: () => Effect.Effect<string, PlatformError>
readonly installation: () => Effect.Effect<Installation | undefined, PlatformError>
readonly open: () => Effect.Effect<void, PlatformError>
}
export class Service extends Context.Service<Service, Interface>()("@kilocode/AnacondaDesktopPlatform") {}
function value(info: Info, key: string) {
const direct = info.env[key]
if (direct) return direct
const found = Object.entries(info.env).find(([name, item]) => name.toLowerCase() === key.toLowerCase() && item)
return found?.[1]
}
export function supported(info: Pick<Info, "platform" | "arch">) {
if (info.platform === "darwin") return info.arch === "arm64"
if (info.platform === "win32") return info.arch === "x64"
if (info.platform === "linux") return info.arch === "x64" || info.arch === "arm64"
return false
}
export function directory(info: Info) {
if (info.platform === "darwin") return path.join(info.home, "Library", "Application Support", "anaconda-desktop")
if (info.platform === "win32") {
const root =
value(info, "APPDATA") ?? path.win32.join(value(info, "USERPROFILE") ?? info.home, "AppData", "Roaming")
return path.win32.join(root, "anaconda-desktop")
}
if (info.platform === "linux") {
const root = value(info, "XDG_DATA_HOME") ?? path.posix.join(info.home, ".local", "share")
return path.posix.join(root, "anaconda-desktop")
}
}
export function candidates(info: Info) {
if (info.platform === "darwin") {
return ["/Applications/Anaconda Desktop.app", path.join(info.home, "Applications", "Anaconda Desktop.app")]
}
if (info.platform === "win32") {
const local = value(info, "LOCALAPPDATA") ?? path.win32.join(info.home, "AppData", "Local")
const program = value(info, "ProgramFiles") ?? "C:\\Program Files"
const x86 = value(info, "ProgramFiles(x86)")
return [
path.win32.join(local, "Programs", "Anaconda Desktop", "Anaconda Desktop.exe"),
path.win32.join(local, "anaconda-desktop", "Anaconda Desktop.exe"),
path.win32.join(program, "Anaconda Desktop", "Anaconda Desktop.exe"),
...(x86 ? [path.win32.join(x86, "Anaconda Desktop", "Anaconda Desktop.exe")] : []),
]
}
if (info.platform === "linux") {
const env = value(info, "PATH")?.split(path.posix.delimiter).filter(Boolean) ?? []
return [
...env.map((dir) => path.posix.join(dir, "anaconda-desktop")),
"/usr/bin/anaconda-desktop",
"/usr/local/bin/anaconda-desktop",
path.posix.join(info.home, ".local", "bin", "anaconda-desktop"),
]
}
return []
}
export function command(info: Info, install: Installation) {
if (info.platform === "darwin") return ["/usr/bin/open", install.path]
if (info.platform === "win32") return [install.path]
if (info.platform === "linux") {
const wayland = value(info, "XDG_SESSION_TYPE")?.toLowerCase() === "wayland" || !!value(info, "WAYLAND_DISPLAY")
return [install.path, ...(wayland ? ["--ozone-platform=x11"] : [])]
}
}
const variables = [
"APPDATA",
"ComSpec",
"DBUS_SESSION_BUS_ADDRESS",
"DESKTOP_SESSION",
"DISPLAY",
"GDMSESSION",
"HOME",
"HOMEDRIVE",
"HOMEPATH",
"LANG",
"LC_ALL",
"LC_CTYPE",
"LOCALAPPDATA",
"PATH",
"PATHEXT",
"ProgramData",
"ProgramFiles",
"ProgramFiles(x86)",
"SHELL",
"SystemRoot",
"TEMP",
"TMP",
"TMPDIR",
"USERPROFILE",
"WAYLAND_DISPLAY",
"WINDIR",
"XAUTHORITY",
"XDG_CACHE_HOME",
"XDG_CONFIG_HOME",
"XDG_CURRENT_DESKTOP",
"XDG_DATA_HOME",
"XDG_RUNTIME_DIR",
"XDG_STATE_HOME",
"XDG_SESSION_TYPE",
] as const
export function environment(info: Info): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = Object.fromEntries(Object.keys(info.env).map((key) => [key, undefined]))
for (const key of variables) {
const item = value(info, key)
if (item) env[key] = item
}
if (info.platform === "win32") env.USERPROFILE ??= info.home
else env.HOME ??= info.home
return env
}
function current(): Info {
return {
platform: process.platform,
arch: arch(),
home: homedir(),
env: process.env,
}
}
export function makeLayer(info: Info) {
return Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const dataDir = Effect.fn("AnacondaDesktopPlatform.dataDir")(function* () {
const dir = directory(info)
if (dir) return dir
return yield* new PlatformError({ operation: "data-dir", reason: "unsupported" })
})
const installation = Effect.fn("AnacondaDesktopPlatform.installation")(function* () {
if (!supported(info)) {
return yield* new PlatformError({ operation: "installation", reason: "unsupported" })
}
for (const candidate of candidates(info)) {
if (yield* fs.existsSafe(candidate)) return { path: candidate }
}
return undefined
})
const open = Effect.fn("AnacondaDesktopPlatform.open")(function* () {
const install = yield* installation()
if (!install) return yield* new PlatformError({ operation: "open", reason: "not-installed" })
const cmd = command(info, install)
if (!cmd) return yield* new PlatformError({ operation: "open", reason: "unsupported" })
const child = yield* Effect.try({
try: () => Process.spawn(cmd, { env: environment(info) }),
catch: () => new PlatformError({ operation: "open", reason: "failed" }),
})
yield* Effect.callback<void, PlatformError>((resume) => {
const done = () => {
child.removeListener("error", fail)
child.unref()
resume(Effect.void)
}
const fail = () => {
child.removeListener("spawn", done)
resume(Effect.fail(new PlatformError({ operation: "open", reason: "failed" })))
}
child.once("spawn", done)
child.once("error", fail)
return Effect.sync(() => {
child.removeListener("spawn", done)
child.removeListener("error", fail)
})
})
})
return Service.of({ info, dataDir, installation, open })
}),
)
}
export const layer = makeLayer(current())
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
@@ -0,0 +1,116 @@
import type { Hooks, Plugin } from "@kilocode/plugin"
import type { Model } from "@kilocode/sdk/v2"
import type { Provider } from "@opencode-ai/core/models-dev"
import { decodeMetadata, PROVIDER_ID, type Metadata, type Modality, type ModelDescriptor } from "./domain"
export const PLACEHOLDER_MODEL_ID = "setup-required"
export const CatalogProvider = {
id: PROVIDER_ID,
name: "Anaconda Desktop",
description: "Run models served by Anaconda Desktop on this machine.",
env: [],
api: "",
npm: "@ai-sdk/openai-compatible",
models: {
[PLACEHOLDER_MODEL_ID]: {
id: PLACEHOLDER_MODEL_ID,
name: "Set up Anaconda Desktop",
family: PROVIDER_ID,
release_date: "",
attachment: false,
reasoning: false,
temperature: true,
tool_call: false,
cost: { input: 0, output: 0 },
limit: { context: 0, output: 0 },
modalities: { input: ["text"], output: ["text"] },
},
},
} satisfies Provider
export function overlay(providers: Record<string, Provider>): Record<string, Provider> {
return { ...providers, [PROVIDER_ID]: CatalogProvider }
}
function model(input: ModelDescriptor, metadata: Metadata): Model {
const includes = (items: ReadonlyArray<Modality>, value: Modality) => items.includes(value)
return {
id: input.id,
providerID: PROVIDER_ID,
api: {
id: input.id,
url: metadata.baseURL,
npm: "@ai-sdk/openai-compatible",
},
name: input.name,
...(input.family ? { family: input.family } : {}),
capabilities: {
temperature: true,
reasoning: false,
attachment: input.input.some((item) => item !== "text"),
toolcall: metadata.toolcall === "supported",
input: {
text: includes(input.input, "text"),
audio: includes(input.input, "audio"),
image: includes(input.input, "image"),
video: includes(input.input, "video"),
pdf: includes(input.input, "pdf"),
},
output: {
text: includes(input.output, "text"),
audio: includes(input.output, "audio"),
image: includes(input.output, "image"),
video: includes(input.output, "video"),
pdf: includes(input.output, "pdf"),
},
interleaved: false,
},
cost: {
input: 0,
output: 0,
cache: { read: 0, write: 0 },
},
limit: {
context: metadata.context,
output: 0,
},
status: "active",
options: input.description ? { description: input.description } : {},
headers: {},
release_date: "",
variants: {},
}
}
export function hooks(): Hooks {
return {
auth: {
provider: PROVIDER_ID,
methods: [
{
type: "api",
label: "Local Anaconda Desktop",
},
],
async loader(auth) {
const stored = await auth()
if (stored.type !== "api") return {}
const metadata = decodeMetadata("metadata" in stored ? stored.metadata : undefined)
if (!metadata) return {}
return { baseURL: metadata.baseURL }
},
},
provider: {
id: PROVIDER_ID,
async models(_provider, ctx) {
if (ctx.auth?.type !== "api") return {}
const metadata = decodeMetadata("metadata" in ctx.auth ? ctx.auth.metadata : undefined)
if (!metadata) return {}
return Object.fromEntries(metadata.models.map((item) => [item.id, model(item, metadata)]))
},
},
}
}
export const AnacondaDesktopPlugin: Plugin = async () => hooks()
@@ -0,0 +1,92 @@
import { Auth } from "@/auth"
import { invalidateAfterProviderAuthChange } from "@/kilocode/server/provider-auth-lifecycle"
import { InstanceStore } from "@/project/instance-store"
import { ModelCache } from "@/provider/model-cache"
import { Context, Effect, Layer, Redacted } from "effect"
import * as Discovery from "./discovery"
import * as DesktopPlatform from "./platform"
import {
encodeMetadata,
NotReadyError,
PROVIDER_ID,
SyncError,
ToolAcknowledgementError,
type PlatformError,
type ReadyStatus,
type Status,
} from "./domain"
export interface Interface {
readonly status: () => Effect.Effect<Status>
readonly open: () => Effect.Effect<true, PlatformError>
readonly sync: (
acknowledge?: boolean,
) => Effect.Effect<ReadyStatus, NotReadyError | SyncError | ToolAcknowledgementError>
}
export class Service extends Context.Service<Service, Interface>()("@kilocode/AnacondaDesktop") {}
function same(left: Record<string, string> | undefined, right: Record<string, string>) {
if (!left) return false
const keys = Object.keys(right)
if (Object.keys(left).length !== keys.length) return false
return keys.every((key) => left[key] === right[key])
}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const auth = yield* Auth.Service
const cache = yield* ModelCache.Service
const discovery = yield* Discovery.Service
const instances = yield* InstanceStore.Service
const platform = yield* DesktopPlatform.Service
const status = Effect.fn("AnacondaDesktop.status")(function* () {
return (yield* discovery.discover()).status
})
const open = Effect.fn("AnacondaDesktop.open")(function* () {
yield* platform.open()
return true as const
})
const sync = Effect.fn("AnacondaDesktop.sync")(function* (acknowledge = false) {
const found = yield* discovery.discover()
if (found.status.type !== "ready" || !found.connection) {
return yield* new NotReadyError({ status: found.status })
}
if (found.status.toolcall !== "supported" && !acknowledge) {
return yield* new ToolAcknowledgementError({ status: found.status })
}
const metadata = encodeMetadata(found.connection.metadata)
if (!metadata) return yield* new SyncError({ operation: "encode" })
const key = Redacted.value(found.connection.key)
const stored = yield* auth.get(PROVIDER_ID).pipe(Effect.mapError(() => new SyncError({ operation: "store" })))
if (stored?.type === "api" && stored.key === key && same(stored.metadata, metadata)) return found.status
yield* auth
.set(
PROVIDER_ID,
new Auth.Api({
type: "api",
key,
metadata,
}),
)
.pipe(Effect.mapError(() => new SyncError({ operation: "store" })))
yield* invalidateAfterProviderAuthChange(PROVIDER_ID).pipe(
Effect.provideService(ModelCache.Service, cache),
Effect.provideService(InstanceStore.Service, instances),
)
return found.status
})
return Service.of({ status, open, sync })
}),
)
const platform = DesktopPlatform.layer
const discovery = Discovery.layer.pipe(Layer.provide(platform))
export const liveLayer = layer.pipe(Layer.provide(discovery), Layer.provide(platform))
@@ -0,0 +1,248 @@
import type { AnacondaDesktopStatus } from "@kilocode/sdk/v2"
import { DOWNLOAD_URL } from "../domain"
export type ReadyStatus = Extract<AnacondaDesktopStatus, { type: "ready" }>
export type SetupState = {
status?: AnacondaDesktopStatus
phase: "idle" | "checking" | "opening" | "syncing"
error?: string
}
type Api = {
status(signal: AbortSignal): Promise<AnacondaDesktopStatus>
open(signal: AbortSignal): Promise<void>
sync(acknowledge: boolean, signal: AbortSignal): Promise<ReadyStatus>
}
type Options = {
api: Api
synced(status: ReadyStatus, signal: AbortSignal): Promise<void> | void
change?(state: SetupState): void
}
function message(error: unknown) {
if (error instanceof Error && error.message) return error.message
return "The Anaconda Desktop operation failed."
}
export function complete(input: { pick(): void; signal?: AbortSignal }) {
if (input.signal?.aborted) return false
input.pick()
return true
}
export function createSetupController(options: Options) {
let state: SetupState = { phase: "idle" }
let active = false
let busy = false
let abort: AbortController | undefined
const update = (next: Partial<SetupState>) => {
state = { ...state, ...next }
options.change?.(state)
}
const operate = async (phase: SetupState["phase"], task: (signal: AbortSignal) => Promise<void>) => {
if (!active || busy) return false
busy = true
const ctrl = new AbortController()
abort = ctrl
update({ phase, error: undefined })
try {
await task(ctrl.signal)
if (!active || ctrl.signal.aborted) return false
update({ phase: "idle", error: undefined })
return true
} catch (error) {
if (!active || ctrl.signal.aborted) return false
update({ phase: "idle", error: message(error) })
return false
} finally {
if (abort === ctrl) abort = undefined
busy = false
}
}
const check = () =>
operate("checking", async (signal) => {
const status = await options.api.status(signal)
if (!signal.aborted) update({ status })
})
return {
start() {
if (active) return
active = true
void check()
},
stop() {
if (!active) return
active = false
abort?.abort()
abort = undefined
},
refresh: check,
open() {
return operate("opening", (signal) => options.api.open(signal))
},
connect() {
const status = state.status
if (status?.type !== "ready") return Promise.resolve(false)
return operate("syncing", async (signal) => {
const synced = await options.api.sync(status.toolcall !== "supported", signal)
if (active && !signal.aborted) await options.synced(synced, signal)
})
},
snapshot() {
return state
},
}
}
export type SetupAction = "download" | "open" | "connect" | "refresh"
export type SetupView = {
title: string
lines: string[]
actions: Array<{ key: string; label: string; type: SetupAction }>
downloadURL?: string
warning?: boolean
}
const refresh = { key: "r", label: "check again", type: "refresh" } as const
const desktop = { key: "o", label: "open Anaconda Desktop", type: "open" } as const
function ready(status: Extract<AnacondaDesktopStatus, { type: "ready" }>): SetupView {
const models = status.models.map((model) => model.name).join(", ")
const server = status.serverName ? `${status.serverName} (${status.serverID})` : status.serverID
const context = status.context > 0 ? `${status.context.toLocaleString()} tokens` : "not reported"
const base = [`Server: ${server}`, `Models: ${models}`, `Context: ${context}`]
if (status.toolcall === "supported") {
return {
title: "Anaconda Desktop is ready",
lines: [...base, "Tool calling: supported. Connect to import this server into Kilo."],
actions: [{ key: "c", label: "connect / refresh now", type: "connect" }, desktop, refresh],
}
}
const capability = status.toolcall === "unsupported" ? "not supported" : "unknown"
return {
title: "Limited tool support",
lines: [
...base,
`Tool calling: ${capability}.`,
"Coding-agent actions may fail. Continue only if you accept these limitations.",
],
actions: [{ key: "c", label: "connect anyway", type: "connect" }, desktop, refresh],
warning: true,
}
}
export function setupView(status?: AnacondaDesktopStatus): SetupView {
if (!status) {
return {
title: "Connect Anaconda Desktop",
lines: ["Checking this machine for Anaconda Desktop..."],
actions: [refresh],
}
}
switch (status.type) {
case "unsupported-platform":
return {
title: "Platform not supported",
lines: [
`Anaconda Desktop cannot be connected on ${status.platform}.`,
"Local setup is supported on macOS, Windows, and Linux.",
],
actions: [refresh],
}
case "not-installed":
return {
title: "Install Anaconda Desktop",
lines: ["Anaconda Desktop was not found on this machine.", "Download and install it, then check again."],
actions: [{ key: "d", label: "open official download page", type: "download" }, refresh],
downloadURL: DOWNLOAD_URL,
}
case "not-running":
return {
title: "Start Anaconda Desktop",
lines: [
"Anaconda Desktop is installed but is not running.",
"Open it here, then choose check again.",
],
actions: [desktop, refresh],
}
case "invalid-config": {
const reason = {
missing: "Desktop has not created its local configuration yet.",
malformed: "Desktop's local configuration is malformed.",
"missing-key": "Desktop's management credential is missing.",
"invalid-port": "Desktop's management port is invalid.",
}[status.reason]
return {
title: "Finish Desktop setup",
lines: [reason, "Open Anaconda Desktop and finish setup or restart the app."],
actions: [desktop, refresh],
}
}
case "signed-out":
return {
title: "Sign in to Anaconda Desktop",
lines: ["No saved Anaconda Desktop sign-in was found.", "Open Desktop and sign in, then choose check again."],
actions: [desktop, refresh],
}
case "management-unauthorized":
return {
title: "Reconnect Anaconda Desktop",
lines: [
"Desktop rejected its local management credential.",
"Open Desktop, sign in again if needed, and restart it.",
],
actions: [desktop, refresh],
}
case "management-unavailable":
return {
title: "Anaconda Desktop is unavailable",
lines: [
status.reason === "timeout"
? "Desktop did not respond before the local request timed out."
: "Desktop returned an unexpected local response.",
"Open or restart Desktop, then choose check again.",
],
actions: [desktop, refresh],
}
case "no-downloaded-model":
return {
title: "Download a text-generation model",
lines: [
"No downloaded model is available in Anaconda Desktop.",
"Open Desktop and download a text-generation model.",
],
actions: [desktop, refresh],
}
case "no-running-server":
return {
title: "Start a model server",
lines: [
`${status.downloadedModels} downloaded model${status.downloadedModels === 1 ? " is" : "s are"} available.`,
"In Desktop, start a model server. Models with tool calling support are strongly recommended.",
],
actions: [desktop, refresh],
}
case "inference-unhealthy":
return {
title: "Model server is not healthy",
lines: [
`Desktop reports server ${status.serverID}, but it is not ready for chat completions.`,
"Open Desktop and restart or inspect the server.",
],
actions: [desktop, refresh],
}
case "ready":
return ready(status)
}
}
@@ -0,0 +1,124 @@
import { TextAttributes } from "@opentui/core"
import { createMemo, createSignal, For, onCleanup, onMount, Show, type JSX } from "solid-js"
import open from "open"
import { useTheme } from "@tui/context/theme"
import { useSDK } from "@tui/context/sdk"
import { useBindings } from "@tui/keymap"
import { useDialog } from "@tui/ui/dialog"
import { Link } from "@tui/ui/link"
import { useToast } from "@tui/ui/toast"
import { PROVIDER_ID } from "../domain"
import { complete, createSetupController, setupView, type SetupAction, type SetupState } from "./model"
type ModelComponent = (props: { providerID?: string }) => JSX.Element
export function selectProvider(input: {
providerID: string
replace(component: () => JSX.Element): void
model: ModelComponent
}) {
if (input.providerID !== PROVIDER_ID) return false
input.replace(() => <AnacondaDesktopSetup model={input.model} />)
return true
}
function errorMessage(error: unknown, fallback: string) {
if (typeof error !== "object" || error === null || !("message" in error)) return fallback
return typeof error.message === "string" && error.message ? error.message : fallback
}
export function AnacondaDesktopSetup(props: { model: ModelComponent }) {
const sdk = useSDK()
const dialog = useDialog()
const toast = useToast()
const { theme } = useTheme()
const Model = props.model
const [state, setState] = createSignal<SetupState>({ phase: "idle" })
const view = createMemo(() => setupView(state().status))
const controller = createSetupController({
api: {
async status(signal) {
const result = await sdk.client.anacondaDesktop.status(undefined, { signal })
if (result.data) return result.data
throw new Error(errorMessage(result.error, "Anaconda Desktop status could not be checked."))
},
async open(signal) {
const result = await sdk.client.anacondaDesktop.open(undefined, { signal })
if (result.data) return
throw new Error(errorMessage(result.error, "Anaconda Desktop could not be opened."))
},
async sync(acknowledgeToolLimitations, signal) {
const result = await sdk.client.anacondaDesktop.sync({ acknowledgeToolLimitations }, { signal })
if (result.data) return result.data
throw new Error(errorMessage(result.error, "The Anaconda Desktop connection could not be synchronized."))
},
},
change: setState,
synced(_, signal) {
complete({
pick: () => dialog.replace(() => <Model providerID={PROVIDER_ID} />),
signal,
})
},
})
const run = (action: SetupAction) => {
if (action === "refresh") return void controller.refresh()
if (action === "open") return void controller.open()
if (action === "connect") return void controller.connect()
const url = view().downloadURL
if (!url) return
void open(url).catch(toast.error)
}
useBindings(() => ({
bindings: view().actions.map((action) => ({
key: action.key,
desc: action.label,
group: "Dialog",
cmd: () => run(action.type),
})),
}))
onMount(controller.start)
onCleanup(controller.stop)
const activity = createMemo(() => {
if (state().phase === "checking") return "Checking Desktop status..."
if (state().phase === "opening") return "Opening Anaconda Desktop..."
if (state().phase === "syncing") return "Synchronizing provider and models..."
})
return (
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={view().warning ? theme.warning : theme.text}>
{view().title}
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box gap={0}>
<For each={view().lines}>{(line) => <text fg={theme.textMuted}>{line}</text>}</For>
</box>
<Show when={view().downloadURL}>{(url) => <Link href={url()} fg={theme.primary} wrapMode="word" />}</Show>
<Show when={state().error}>{(error) => <text fg={theme.error}>{error()}</text>}</Show>
<Show when={activity()}>{(message) => <text fg={theme.textMuted}>{message()}</text>}</Show>
<box gap={0}>
<For each={view().actions}>
{(action) => (
<text fg={theme.text} onMouseUp={() => run(action.type)}>
{action.key} <span style={{ fg: theme.textMuted }}>{action.label}</span>
</text>
)}
</For>
</box>
</box>
)
}
@@ -1,4 +1,3 @@
// kilocode_change - new file
/**
* Kilo-specific overrides for the provider dialog.
*
@@ -10,6 +9,7 @@ import type { JSX } from "solid-js"
import type { RGBA } from "@opentui/core"
import type { ProviderAuthAuthorization } from "@kilocode/sdk/v2"
import { KiloAutoMethod } from "@/kilocode/components/dialog-kilo-auto-method"
export { selectProvider } from "@/kilocode/anaconda-desktop/tui/setup"
// ---------------------------------------------------------------------------
// Failed-state gutter/description helpers
@@ -51,6 +51,7 @@ export const PROVIDER_PRIORITY: Record<string, number> = {
"github-copilot": 1,
openai: 2,
google: 3,
"anaconda-desktop": 4,
}
// ---------------------------------------------------------------------------
@@ -61,6 +62,7 @@ export const PROVIDER_DESCRIPTIONS: Record<string, string> = {
kilo: "(Recommended)",
anthropic: "(Claude Max or API key)",
openai: "(ChatGPT login or API key)",
"anaconda-desktop": "(Local models)",
}
export const PROVIDER_TITLES: Record<string, string> = {
@@ -16,9 +16,10 @@ const notes: Record<string, string> = {
google: "settings.providers.note.google",
openrouter: "settings.providers.note.openrouter",
vercel: "settings.providers.note.vercel",
"anaconda-desktop": "settings.providers.note.anacondaDesktop",
}
const order = ["kilo", "anthropic", "deepseek", "openai", "google", "openrouter", "vercel"] as const
const order = ["kilo", "anthropic", "deepseek", "openai", "google", "anaconda-desktop", "openrouter", "vercel"] as const
const priority = new Map<string, number>(order.map((id, index) => [id, index]))
@@ -34,7 +35,7 @@ export function providerMetadata(id: string): ProviderMetadata {
const note = notes[name]
return {
noteKey: note,
icon: icons.has(name as IconName) ? name : "synthetic",
icon: name === "anaconda-desktop" ? "inference" : icons.has(name as IconName) ? name : "synthetic",
priority: priority.get(name),
}
}
@@ -0,0 +1,100 @@
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { ReadyStatus, Status } from "@/kilocode/anaconda-desktop/domain"
import { Authorization } from "@/server/routes/instance/httpapi/middleware/authorization"
import { InstanceContextMiddleware } from "@/server/routes/instance/httpapi/middleware/instance-context"
import {
WorkspaceRoutingMiddleware,
WorkspaceRoutingQuery,
} from "@/server/routes/instance/httpapi/middleware/workspace-routing"
import { described } from "@/server/routes/instance/httpapi/groups/metadata"
const root = "/kilocode/anaconda-desktop"
export const AnacondaDesktopPaths = {
status: `${root}/status`,
open: `${root}/open`,
sync: `${root}/sync`,
} as const
export const AnacondaDesktopSyncPayload = Schema.Struct({
acknowledgeToolLimitations: Schema.optional(Schema.Boolean),
})
export class AnacondaDesktopConflictError extends Schema.ErrorClass<AnacondaDesktopConflictError>(
"AnacondaDesktopConflictError",
)(
{
code: Schema.Literals(["unsupported-platform", "not-installed", "not-ready", "acknowledgement-required"]),
message: Schema.String,
status: Schema.optional(Status),
},
{ httpApiStatus: 409 },
) {}
export class AnacondaDesktopOperationError extends Schema.ErrorClass<AnacondaDesktopOperationError>(
"AnacondaDesktopOperationError",
)(
{
operation: Schema.Literals(["open", "sync"]),
message: Schema.String,
},
{ httpApiStatus: 500 },
) {}
export const AnacondaDesktopApi = HttpApi.make("anaconda-desktop")
.add(
HttpApiGroup.make("anaconda-desktop")
.add(
HttpApiEndpoint.get("status", AnacondaDesktopPaths.status, {
query: WorkspaceRoutingQuery,
success: described(Status, "Anaconda Desktop setup status"),
}).annotateMerge(
OpenApi.annotations({
identifier: "anacondaDesktop.status",
summary: "Get Anaconda Desktop setup status",
description: "Discover the locally installed Anaconda Desktop and its active inference server.",
}),
),
HttpApiEndpoint.post("open", AnacondaDesktopPaths.open, {
query: WorkspaceRoutingQuery,
success: described(Schema.Literal(true), "Anaconda Desktop opened"),
error: [AnacondaDesktopConflictError, AnacondaDesktopOperationError],
}).annotateMerge(
OpenApi.annotations({
identifier: "anacondaDesktop.open",
summary: "Open Anaconda Desktop",
description: "Open the locally installed Anaconda Desktop application.",
}),
),
HttpApiEndpoint.post("sync", AnacondaDesktopPaths.sync, {
query: WorkspaceRoutingQuery,
payload: AnacondaDesktopSyncPayload,
success: described(ReadyStatus, "Anaconda Desktop connection synchronized"),
error: [AnacondaDesktopConflictError, AnacondaDesktopOperationError],
}).annotateMerge(
OpenApi.annotations({
identifier: "anacondaDesktop.sync",
summary: "Synchronize Anaconda Desktop provider",
description:
"Discover the active local inference server and replace Kilo provider authentication metadata.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "anaconda-desktop",
description: "Local Anaconda Desktop provider setup routes.",
}),
)
.middleware(InstanceContextMiddleware)
.middleware(WorkspaceRoutingMiddleware)
.middleware(Authorization),
)
.annotateMerge(
OpenApi.annotations({
title: "kilo HttpApi",
version: "0.0.1",
description: "Kilo HttpApi surface.",
}),
)
@@ -7,6 +7,7 @@ import {
WorkspaceRoutingQuery,
} from "@/server/routes/instance/httpapi/middleware/workspace-routing"
import { described } from "@/server/routes/instance/httpapi/groups/metadata"
import { AnacondaDesktopApi } from "./anaconda-desktop"
import {
Failure as NotebookFailure,
Request as NotebookRequest,
@@ -123,6 +124,7 @@ export const KilocodeApi = HttpApi.make("kilocode")
.middleware(WorkspaceRoutingMiddleware)
.middleware(Authorization),
)
.addHttpApi(AnacondaDesktopApi)
.annotateMerge(
OpenApi.annotations({
title: "kilo HttpApi",
@@ -0,0 +1,59 @@
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import * as AnacondaDesktop from "@/kilocode/anaconda-desktop/service"
import { NotReadyError, PlatformError, SyncError, ToolAcknowledgementError } from "@/kilocode/anaconda-desktop/domain"
import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api"
import { AnacondaDesktopConflictError, AnacondaDesktopOperationError } from "../groups/anaconda-desktop"
function openError(error: PlatformError) {
if (error.reason === "unsupported") {
return new AnacondaDesktopConflictError({
code: "unsupported-platform",
message: "Anaconda Desktop is not supported on this platform.",
})
}
if (error.reason === "not-installed") {
return new AnacondaDesktopConflictError({
code: "not-installed",
message: "Anaconda Desktop is not installed.",
})
}
return new AnacondaDesktopOperationError({
operation: "open",
message: "Anaconda Desktop could not be opened.",
})
}
function syncError(error: NotReadyError | SyncError | ToolAcknowledgementError) {
if (error instanceof NotReadyError) {
return new AnacondaDesktopConflictError({
code: "not-ready",
message: "Anaconda Desktop does not have a healthy text-generation server ready.",
status: error.status,
})
}
if (error instanceof ToolAcknowledgementError) {
return new AnacondaDesktopConflictError({
code: "acknowledgement-required",
message: "Acknowledge limited tool support before connecting this model server.",
status: error.status,
})
}
return new AnacondaDesktopOperationError({
operation: "sync",
message: "The Anaconda Desktop connection could not be stored.",
})
}
export const anacondaDesktopHandlers = HttpApiBuilder.group(InstanceHttpApi, "anaconda-desktop", (handlers) =>
Effect.gen(function* () {
const desktop = yield* AnacondaDesktop.Service
return handlers
.handle("status", () => desktop.status())
.handle("open", () => desktop.open().pipe(Effect.mapError(openError)))
.handle("sync", (ctx) =>
desktop.sync(ctx.payload.acknowledgeToolLimitations === true).pipe(Effect.mapError(syncError)),
)
}),
)
@@ -5,8 +5,10 @@ import { compressionLayer } from "@/server/routes/instance/httpapi/middleware/co
import { corsVaryFix } from "@/server/routes/instance/httpapi/middleware/cors-vary"
import { errorLayer } from "@/server/routes/instance/httpapi/middleware/error"
import { fenceLayer } from "@/server/routes/instance/httpapi/middleware/fence"
import * as AnacondaDesktop from "@/kilocode/anaconda-desktop/service"
import { agentBuilderHandlers } from "./handlers/agent-builder"
import { anacondaDesktopHandlers } from "./handlers/anaconda-desktop"
import { backgroundProcessHandlers } from "./handlers/background-process"
import { commitMessageHandlers } from "./handlers/commit-message"
import { configConsoleHandlers } from "./handlers/config-console"
@@ -23,6 +25,7 @@ import { telemetryHandlers } from "./handlers/telemetry"
export const provide = Layer.provide([
agentBuilderHandlers,
anacondaDesktopHandlers.pipe(Layer.provide(AnacondaDesktop.liveLayer)),
backgroundProcessHandlers,
commitMessageHandlers,
configConsoleHandlers,
+2
View File
@@ -28,6 +28,7 @@ import { PluginLoader } from "./loader"
import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
import { KiloAuthPlugin } from "@kilocode/kilo-gateway" // kilocode_change
import { AtomicChatPlugin } from "@kilocode/plugin-atomic-chat" // kilocode_change
import { AnacondaDesktopPlugin } from "@/kilocode/anaconda-desktop/provider" // kilocode_change
import { registerAdapter } from "@/control-plane/adapters"
import type { WorkspaceAdapter } from "@/control-plane/types"
import { RuntimeFlags } from "@/effect/runtime-flags"
@@ -69,6 +70,7 @@ function internalPlugins(flags: RuntimeFlags.Info): PluginInstance[] {
return [
KiloAuthPlugin, // kilocode_change
AtomicChatPlugin, // kilocode_change
AnacondaDesktopPlugin, // kilocode_change
// Temporary rollout: pre-release builds use WebSockets by default; releases require explicit opt-in.
(input) =>
CodexAuthPlugin(input, {
+2 -1
View File
@@ -5,6 +5,7 @@ import { ModelCache } from "./model-cache"
import * as Core from "@opencode-ai/core/models-dev"
import { Context, Effect, Layer } from "effect"
import { AI_SDK_PROVIDERS, KILO_OPENROUTER_BASE, PROMPTS } from "@kilocode/kilo-gateway"
import { overlay } from "@/kilocode/anaconda-desktop/provider"
export const Model = Core.Model
export type Model = Core.Model
@@ -40,7 +41,7 @@ export const layer: Layer.Layer<Service, never, Core.Service | Config.Service |
const cache = yield* ModelCache.Service
const get = Effect.fn("ModelsDev.get")(function* () {
const providers = { ...(yield* core.get()) }
const providers = overlay(yield* core.get())
delete providers.kilo
const cfg = yield* config.get()
@@ -8,6 +8,7 @@ import { ModelCache } from "@/provider/model-cache" // kilocode_change
import { disposeAllInstancesAfterProviderAuthCallback } from "@/kilocode/server/provider-auth-lifecycle" // kilocode_change
import { providerMetadata } from "@/kilocode/provider/metadata" // kilocode_change
import { filterPromptTrainingModels } from "@/kilocode/provider/model-filter" // kilocode_change
import { overlay as overlayAnacondaDesktop } from "@/kilocode/anaconda-desktop/provider" // kilocode_change
import { Effect, Schema } from "effect"
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApiBuilder } from "effect/unstable/httpapi"
@@ -43,7 +44,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider"
const list = Effect.fn("ProviderHttpApi.list")(function* () {
const config = yield* cfg.get()
const all = yield* ModelsDev.Service.use((s) => s.get())
const all = overlayAnacondaDesktop(yield* ModelsDev.Service.use((s) => s.get())) // kilocode_change
const disabled = new Set(config.disabled_providers ?? [])
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
const filtered: Record<string, (typeof all)[string]> = {}
@@ -0,0 +1,358 @@
import { expect } from "bun:test"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FetchHttpClient } from "effect/unstable/http"
import { Effect, Layer, Redacted } from "effect"
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import * as Discovery from "../../../src/kilocode/anaconda-desktop/discovery"
import * as DesktopPlatform from "../../../src/kilocode/anaconda-desktop/platform"
import { CONFIG_FILE, STORE_FILE } from "../../../src/kilocode/anaconda-desktop/domain"
import { testEffect } from "../../lib/effect"
const it = testEffect(Layer.empty)
const managementKey = "fixture-management-key"
const inferenceKey = "fixture-inference-key"
interface Settings {
readonly installed?: boolean
readonly config?: unknown
readonly signed?: boolean
readonly rootStatus?: number
readonly root?: unknown
readonly closed?: boolean
readonly models?: ReadonlyArray<unknown>
readonly servers?: (port: number) => ReadonlyArray<unknown>
readonly health?: unknown
readonly inferenceModels?: ReadonlyArray<unknown>
readonly props?: unknown
readonly delay?: number
}
function defaults(port: number) {
return [
{
serverProcessId: 4242,
status: "RUNNING",
tag: "inference",
modelFile: { id: "file-1", name: "model-q4.gguf" },
server: { host: "0.0.0.0", port, api_key: inferenceKey },
},
]
}
function fixture(settings: Settings = {}) {
return Effect.acquireRelease(
Effect.promise(() => mkdtemp(path.join(os.tmpdir(), "anaconda-desktop-test-"))),
(dir) => Effect.promise(() => rm(dir, { recursive: true, force: true })),
).pipe(
Effect.flatMap((home) =>
Effect.acquireRelease(
Effect.sync(() => {
const hits: Array<{ method: string; path: string; authorized: boolean }> = []
const inference = Bun.serve({
port: 0,
fetch(request) {
const url = new URL(request.url)
hits.push({
method: request.method,
path: url.pathname,
authorized: request.headers.get("authorization") === `Bearer ${inferenceKey}`,
})
if (url.pathname === "/health") return Response.json(settings.health ?? { status: "ok" })
if (url.pathname === "/v1/models") {
return Response.json({
data: settings.inferenceModels ?? [{ id: "model-q4.gguf", owned_by: "llamacpp" }],
})
}
if (url.pathname === "/props") {
return Response.json(
settings.props ?? {
default_generation_settings: { n_ctx: 16_384 },
chat_template_caps: { supports_tools: true },
modalities: { vision: true, audio: false },
},
)
}
return new Response(null, { status: 404 })
},
})
const port = inference.port
if (port === undefined) throw new Error("inference fixture did not bind a port")
const management = Bun.serve({
port: 0,
async fetch(request) {
const url = new URL(request.url)
hits.push({
method: request.method,
path: url.pathname,
authorized: request.headers.get("authorization") === `Bearer ${managementKey}`,
})
if (settings.delay) await Bun.sleep(settings.delay)
if (url.pathname === "/api") {
return Response.json(settings.root ?? { data: { version: "fixture" } }, {
status: settings.rootStatus ?? 200,
})
}
if (url.pathname === "/api/models") {
return Response.json({
data: settings.models ?? [
{
id: "model-1",
name: "Fixture Model",
metadata: {
trainedFor: "text-generation",
contextWindowSize: 8192,
model_type: "fixture-family",
quantizations: [{ modelFileName: "model-q4.gguf" }],
},
},
],
})
}
if (url.pathname === "/api/servers") {
return Response.json({ data: settings.servers?.(port) ?? defaults(port) })
}
return new Response(null, { status: 404 })
},
})
const managementPort = management.port
if (managementPort === undefined) {
management.stop(true)
inference.stop(true)
throw new Error("management fixture did not bind a port")
}
return { home, hits, inference, inferencePort: port, management, managementPort }
}),
(value) =>
Effect.sync(() => {
value.management.stop(true)
value.inference.stop(true)
}),
).pipe(
Effect.tap((value) =>
Effect.promise(async () => {
const dir = path.join(value.home, ".local", "share", "anaconda-desktop")
const bin = path.join(value.home, "bin")
await mkdir(dir, { recursive: true })
await mkdir(bin, { recursive: true })
if (settings.installed !== false) await writeFile(path.join(bin, "anaconda-desktop"), "fixture")
await writeFile(
path.join(dir, CONFIG_FILE),
JSON.stringify(
settings.config ?? {
aiNavApiKey: managementKey,
aiNavApiServerPort: value.managementPort,
},
),
)
if (settings.signed !== false) {
await writeFile(
path.join(dir, STORE_FILE),
JSON.stringify({ "fixture_ai-navigator-workos-oauth": "opaque-fixture" }),
)
}
if (settings.closed) value.management.stop(true)
}),
),
Effect.map((value) => {
const info: DesktopPlatform.Info = {
platform: "linux",
arch: "x64",
home: value.home,
env: { PATH: path.join(value.home, "bin") },
}
const platform = DesktopPlatform.makeLayer(info).pipe(Layer.provide(AppFileSystem.defaultLayer))
const layer = Discovery.makeLayer({ timeout: "100 millis" }).pipe(
Layer.provide(platform),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(FetchHttpClient.layer),
)
return { ...value, layer }
}),
),
),
)
}
it.live("discovers a healthy text-generation server without exposing either key", () =>
Effect.gen(function* () {
const test = yield* fixture()
const found = yield* Discovery.Service.use((service) => service.discover()).pipe(Effect.provide(test.layer))
expect(found.status).toEqual({
type: "ready",
serverID: "process-4242",
serverName: "Fixture Model",
models: [{ id: "model-q4.gguf", name: "Fixture Model" }],
context: 16_384,
toolcall: "supported",
})
expect(found.connection?.metadata).toMatchObject({
baseURL: `http://127.0.0.1:${test.inferencePort}/v1`,
context: 16_384,
toolcall: "supported",
models: [{ family: "fixture-family", input: ["text", "image"], output: ["text"] }],
})
expect(found.connection && Redacted.isRedacted(found.connection.key)).toBe(true)
const serialized = JSON.stringify(found)
expect(serialized).not.toContain(managementKey)
expect(serialized).not.toContain(inferenceKey)
expect(test.hits.every((hit) => hit.method === "GET")).toBe(true)
expect(test.hits.filter((hit) => hit.path.startsWith("/api")).every((hit) => hit.authorized)).toBe(true)
expect(test.hits.filter((hit) => !hit.path.startsWith("/api")).every((hit) => hit.authorized)).toBe(true)
}),
)
it.live("classifies setup states before inference discovery", () =>
Effect.gen(function* () {
const absent = yield* fixture({ installed: false })
const absentStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(
Effect.provide(absent.layer),
)
expect(absentStatus.status.type).toBe("not-installed")
const invalid = yield* fixture({ config: { aiNavApiKey: "", aiNavApiServerPort: 8001 } })
const invalidStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(
Effect.provide(invalid.layer),
)
expect(invalidStatus.status).toEqual({ type: "invalid-config", reason: "missing-key" })
const signed = yield* fixture({ signed: false })
const signedStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(
Effect.provide(signed.layer),
)
expect(signedStatus.status).toEqual({ type: "signed-out" })
const unauthorized = yield* fixture({ rootStatus: 401 })
const unauthorizedStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(
Effect.provide(unauthorized.layer),
)
expect(unauthorizedStatus.status).toEqual({ type: "management-unauthorized" })
const stopped = yield* fixture({ closed: true })
const stoppedStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(
Effect.provide(stopped.layer),
)
expect(stoppedStatus.status).toEqual({ type: "not-running" })
const malformed = yield* fixture({ root: { unexpected: true } })
const malformedStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(
Effect.provide(malformed.layer),
)
expect(malformedStatus.status).toEqual({
type: "management-unavailable",
reason: "unexpected-response",
})
}),
)
it.live("classifies downloaded and running-server inventory", () =>
Effect.gen(function* () {
const empty = yield* fixture({ models: [] })
const emptyStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(Effect.provide(empty.layer))
expect(emptyStatus.status).toEqual({ type: "no-downloaded-model" })
const stopped = yield* fixture({
servers: () => [],
models: [
{
id: "active",
name: "Active",
metadata: { trainedFor: "text-generation", files: [{ name: "active.gguf" }] },
},
{
id: "deleted",
name: "Deleted",
metadata: { trainedFor: "text-generation", files: [] },
},
{
id: "embedding",
name: "Embedding",
metadata: { trainedFor: "sentence-similarity", files: [{ name: "embedding.gguf" }] },
},
],
})
const stoppedStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(
Effect.provide(stopped.layer),
)
expect(stoppedStatus.status).toEqual({ type: "no-running-server", downloadedModels: 1 })
}),
)
it.live("marks unusable inference servers unhealthy", () =>
Effect.gen(function* () {
const remote = yield* fixture({
servers: (port) => [{ ...defaults(port)[0], server: { host: "192.168.1.10", port, apiKey: inferenceKey } }],
})
const remoteStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(
Effect.provide(remote.layer),
)
expect(remoteStatus.status).toMatchObject({ type: "inference-unhealthy" })
const empty = yield* fixture({ inferenceModels: [] })
const emptyStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(Effect.provide(empty.layer))
expect(emptyStatus.status).toMatchObject({ type: "inference-unhealthy" })
const embedding = yield* fixture({
models: [
{
id: "embed",
name: "Embed",
metadata: {
trainedFor: "sentence-similarity",
quantizations: [{ modelFileName: "model-q4.gguf" }],
},
},
],
})
const embeddingStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(
Effect.provide(embedding.layer),
)
expect(embeddingStatus.status).toEqual({ type: "no-downloaded-model" })
const unhealthy = yield* fixture({ health: { status: "loading" } })
const unhealthyStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(
Effect.provide(unhealthy.layer),
)
expect(unhealthyStatus.status).toMatchObject({ type: "inference-unhealthy" })
}),
)
it.live("distinguishes false and unknown tool support and accepts an empty inference key", () =>
Effect.gen(function* () {
const unsupported = yield* fixture({ props: { chat_template_caps: { supports_tools: false } } })
const unsupportedStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(
Effect.provide(unsupported.layer),
)
expect(unsupportedStatus.status).toMatchObject({ type: "ready", toolcall: "unsupported" })
expect(unsupportedStatus.connection?.metadata.models[0].description).toContain("does not support tool calling")
const unknown = yield* fixture({
props: {},
servers: (port) => [
{
id: "server-1",
status: "running",
tag: "inference",
modelFile: { name: "model-q4.gguf" },
serverConfig: { apiParams: { host: "127.0.0.1", port, apiKey: "" } },
},
],
})
const unknownStatus = yield* Discovery.Service.use((service) => service.discover()).pipe(
Effect.provide(unknown.layer),
)
expect(unknownStatus.status).toMatchObject({ type: "ready", toolcall: "unknown" })
expect(unknownStatus.connection && Redacted.value(unknownStatus.connection.key)).toBe("")
expect(unknown.hits.filter((hit) => !hit.path.startsWith("/api")).every((hit) => !hit.authorized)).toBe(true)
}),
)
it.live("bounds management calls with a typed timeout state", () =>
Effect.gen(function* () {
const slow = yield* fixture({ delay: 200 })
const status = yield* Discovery.Service.use((service) => service.discover()).pipe(Effect.provide(slow.layer))
expect(status.status).toEqual({ type: "management-unavailable", reason: "timeout" })
}),
)
@@ -0,0 +1,161 @@
import { describe, expect, test } from "bun:test"
import path from "node:path"
import { Effect, Redacted, Result } from "effect"
import {
decodeMetadata,
encodeMetadata,
endpoint,
normalizeLoopbackEndpoint,
parseConfig,
parseStore,
type Metadata,
} from "../../../src/kilocode/anaconda-desktop/domain"
import {
candidates,
command,
directory,
environment,
supported,
type Info,
} from "../../../src/kilocode/anaconda-desktop/platform"
import { Process } from "../../../src/util/process"
const linux = (env: NodeJS.ProcessEnv = {}): Info => ({
platform: "linux",
arch: "x64",
home: "/home/kilo",
env,
})
const metadata: Metadata = {
version: "1",
serverID: "server-1",
baseURL: "http://127.0.0.1:8080/v1",
models: [
{
id: "local-model",
name: "Local Model",
input: ["text"],
output: ["text"],
description: "Tool-call support is unknown.",
},
],
context: 8192,
toolcall: "unknown",
}
describe("Anaconda Desktop config", () => {
test("strictly parses the management key and port into a redacted value", () => {
const parsed = Effect.runSync(
parseConfig(JSON.stringify({ aiNavApiKey: "test-management-key", aiNavApiServerPort: 8001 })),
)
expect(Redacted.isRedacted(parsed.aiNavApiKey)).toBe(true)
expect(Redacted.value(parsed.aiNavApiKey)).toBe("test-management-key")
expect(parsed.aiNavApiServerPort).toBe(8001)
expect(JSON.stringify(parsed)).not.toContain("test-management-key")
})
test("classifies malformed, missing-key, and invalid-port values without echoing input", () => {
const cases = [
["not-json:test-management-key", "malformed"],
[JSON.stringify({ aiNavApiKey: "", aiNavApiServerPort: 8001 }), "missing-key"],
[JSON.stringify({ aiNavApiKey: "test-management-key", aiNavApiServerPort: "8001" }), "invalid-port"],
[JSON.stringify({ aiNavApiKey: "test-management-key", aiNavApiServerPort: 70_000 }), "invalid-port"],
] as const
for (const [input, reason] of cases) {
const result = Effect.runSync(Effect.result(parseConfig(input)))
expect(Result.isFailure(result)).toBe(true)
if (Result.isFailure(result)) {
expect(result.failure.reason).toBe(reason)
expect(JSON.stringify(result.failure)).not.toContain("test-management-key")
}
}
})
test("checks only opaque non-empty OAuth entries with the required suffix", () => {
expect(
Effect.runSync(parseStore(JSON.stringify({ "account_ai-navigator-workos-oauth": "opaque-encrypted-value" }))),
).toBe(true)
expect(Effect.runSync(parseStore(JSON.stringify({ "account_ai-navigator-workos-oauth": "" })))).toBe(false)
expect(Effect.runSync(parseStore(JSON.stringify({ unrelated: "opaque-encrypted-value" })))).toBe(false)
const result = Effect.runSync(Effect.result(parseStore("[]")))
expect(Result.isFailure(result)).toBe(true)
})
})
describe("Anaconda Desktop metadata", () => {
test("round-trips trusted loopback metadata and rejects malformed or remote records", () => {
const encoded = encodeMetadata(metadata)
expect(encoded).toBeDefined()
if (!encoded) throw new Error("metadata did not encode")
expect(decodeMetadata(encoded)).toEqual(metadata)
expect(decodeMetadata({ ...encoded, version: "2" })).toBeUndefined()
expect(decodeMetadata({ ...encoded, models: "not-json" })).toBeUndefined()
expect(decodeMetadata({ ...encoded, baseURL: "http://example.com:8080/v1" })).toBeUndefined()
expect(endpoint("0.0.0.0", 8080)).toBe("http://127.0.0.1:8080/v1")
expect(endpoint("::", 8080)).toBe("http://[::1]:8080/v1")
expect(normalizeLoopbackEndpoint("http://localhost:8080/")).toBe("http://localhost:8080/v1")
expect(normalizeLoopbackEndpoint("https://127.0.0.1:8080/v1")).toBeUndefined()
expect(normalizeLoopbackEndpoint("http://192.168.1.2:8080/v1")).toBeUndefined()
})
})
describe("Anaconda Desktop platform adapters", () => {
test("resolves official cross-platform user-data locations", () => {
expect(directory({ platform: "darwin", arch: "arm64", home: "/Users/kilo", env: {} })).toBe(
"/Users/kilo/Library/Application Support/anaconda-desktop",
)
expect(
directory({ platform: "win32", arch: "x64", home: "C:\\Users\\kilo", env: { APPDATA: "D:\\Roaming" } }),
).toBe(path.win32.join("D:\\Roaming", "anaconda-desktop"))
expect(directory(linux({ XDG_DATA_HOME: "/data" }))).toBe("/data/anaconda-desktop")
expect(directory(linux())).toBe("/home/kilo/.local/share/anaconda-desktop")
})
test("supports only documented operating-system and architecture pairs", () => {
expect(supported({ platform: "darwin", arch: "arm64" })).toBe(true)
expect(supported({ platform: "darwin", arch: "x64" })).toBe(false)
expect(supported({ platform: "win32", arch: "x64" })).toBe(true)
expect(supported({ platform: "linux", arch: "arm64" })).toBe(true)
expect(supported({ platform: "freebsd", arch: "x64" })).toBe(false)
})
test("builds hidden Windows and Wayland-safe Linux launch commands", () => {
const windows: Info = {
platform: "win32",
arch: "x64",
home: "C:\\Users\\kilo",
env: { LOCALAPPDATA: "C:\\Users\\kilo\\AppData\\Local" },
}
const executable = candidates(windows)[0]
expect(command(windows, { path: executable })).toEqual([executable])
expect(command(linux({ XDG_SESSION_TYPE: "wayland" }), { path: "/usr/bin/anaconda-desktop" })).toEqual([
"/usr/bin/anaconda-desktop",
"--ozone-platform=x11",
])
})
test("passes only required desktop environment variables to the launched app", async () => {
const env = environment(
linux({
PATH: "/usr/bin",
DISPLAY: ":0",
KILO_SERVER_PASSWORD: "secret",
ANTHROPIC_API_KEY: "secret",
}),
)
const output = await Process.run(
[
process.execPath,
"-e",
"process.stdout.write(JSON.stringify({ path: process.env.PATH, secret: process.env.KILO_SERVER_PASSWORD }))",
],
{ env },
)
expect(JSON.parse(output.stdout.toString())).toEqual({ path: "/usr/bin" })
})
})
@@ -0,0 +1,113 @@
import { describe, expect, test } from "bun:test"
import type { Provider } from "@kilocode/sdk/v2"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
import { generateText } from "ai"
import { encodeMetadata, PROVIDER_ID, type Metadata } from "../../../src/kilocode/anaconda-desktop/domain"
import { CatalogProvider, hooks, PLACEHOLDER_MODEL_ID } from "../../../src/kilocode/anaconda-desktop/provider"
const metadata: Metadata = {
version: "1",
serverID: "server-1",
baseURL: "http://127.0.0.1:8080/v1",
models: [
{
id: "model.gguf",
name: "Local Model",
family: "gemma",
input: ["text", "image"],
output: ["text"],
description: "This local model does not support tool calling.",
},
],
context: 131_072,
toolcall: "unsupported",
}
const provider: Provider = {
id: PROVIDER_ID,
name: CatalogProvider.name,
source: "custom",
env: [],
options: {},
models: {},
}
describe("Anaconda Desktop plugin", () => {
test("loads only the safe OpenAI-compatible base URL", async () => {
const plugin = hooks()
const loader = plugin.auth?.loader
if (!loader) throw new Error("auth loader is missing")
const encoded = encodeMetadata(metadata)
if (!encoded) throw new Error("metadata did not encode")
const options = await loader(async () => ({ type: "api", key: "test-inference-key", metadata: encoded }), provider)
expect(options).toEqual({ baseURL: metadata.baseURL })
expect(JSON.stringify(options)).not.toContain("test-inference-key")
})
test("replaces the placeholder with stored active-server models", async () => {
const plugin = hooks()
const load = plugin.provider?.models
if (!load) throw new Error("provider model hook is missing")
const encoded = encodeMetadata(metadata)
if (!encoded) throw new Error("metadata did not encode")
const models = await load(provider, { auth: { type: "api", key: "test-inference-key", metadata: encoded } })
expect(Object.keys(models)).toEqual(["model.gguf"])
expect(models[PLACEHOLDER_MODEL_ID]).toBeUndefined()
expect(models["model.gguf"]).toMatchObject({
providerID: PROVIDER_ID,
api: {
id: "model.gguf",
url: "http://127.0.0.1:8080/v1",
npm: "@ai-sdk/openai-compatible",
},
capabilities: {
attachment: true,
toolcall: false,
input: { text: true, image: true },
output: { text: true },
},
limit: { context: 131_072, output: 0 },
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
})
})
test("uses the stored key through the standard OpenAI-compatible runtime", async () => {
const key = "fixture-standard-runtime-key"
const requests: Array<{ path: string; authorization: string | null }> = []
const server = Bun.serve({
port: 0,
fetch(request) {
requests.push({
path: new URL(request.url).pathname,
authorization: request.headers.get("authorization"),
})
return Response.json({
id: "fixture-completion",
object: "chat.completion",
created: 0,
model: "model.gguf",
choices: [{ index: 0, message: { role: "assistant", content: "ready" }, finish_reason: "stop" }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
})
},
})
try {
const port = server.port
if (port === undefined) throw new Error("runtime fixture did not bind a port")
const sdk = createOpenAICompatible({
name: PROVIDER_ID,
baseURL: `http://127.0.0.1:${port}/v1`,
apiKey: key,
})
const result = await generateText({ model: sdk.languageModel("model.gguf"), prompt: "hello" })
expect(result.text).toBe("ready")
expect(requests).toEqual([{ path: "/v1/chat/completions", authorization: `Bearer ${key}` }])
} finally {
server.stop(true)
}
})
})
@@ -0,0 +1,144 @@
import { expect } from "bun:test"
import { Auth } from "@/auth"
import { InstanceStore } from "@/project/instance-store"
import { ModelCache } from "@/provider/model-cache"
import { Effect, Layer, Redacted, Ref } from "effect"
import * as Discovery from "../../../src/kilocode/anaconda-desktop/discovery"
import {
decodeMetadata,
PROVIDER_ID,
type Metadata,
type ReadyStatus,
} from "../../../src/kilocode/anaconda-desktop/domain"
import * as DesktopPlatform from "../../../src/kilocode/anaconda-desktop/platform"
import * as Desktop from "../../../src/kilocode/anaconda-desktop/service"
import { testEffect } from "../../lib/effect"
const it = testEffect(Layer.empty)
function ready(
serverID: string,
port: number,
key: string,
toolcall: Metadata["toolcall"] = "supported",
): Discovery.DiscoveryResult {
const model = `${serverID}.gguf`
const metadata: Metadata = {
version: "1",
serverID,
baseURL: `http://127.0.0.1:${port}/v1`,
models: [{ id: model, name: serverID, input: ["text"], output: ["text"] }],
context: 8192,
toolcall,
}
const status: ReadyStatus = {
type: "ready",
serverID,
models: [{ id: model, name: serverID }],
context: 8192,
toolcall,
}
return {
status,
connection: { key: Redacted.make(key, { label: "fixture inference key" }), metadata },
}
}
it.live("sync atomically replaces the standard auth record and invalidates provider state", () =>
Effect.gen(function* () {
const index = yield* Ref.make(0)
const records = yield* Ref.make<Record<string, Auth.Info>>({})
const events = yield* Ref.make<string[]>([])
const values = [ready("first", 8080, "first-fixture-key"), ready("second", 8081, "second-fixture-key")]
const discovery = Layer.succeed(
Discovery.Service,
Discovery.Service.of({ discover: () => Ref.get(index).pipe(Effect.map((value) => values[value])) }),
)
const platform = Layer.succeed(
DesktopPlatform.Service,
DesktopPlatform.Service.of({
info: { platform: "linux", arch: "x64", home: "/tmp", env: {} },
dataDir: () => Effect.succeed("/tmp"),
installation: () => Effect.succeed({ path: "/usr/bin/anaconda-desktop" }),
open: () => Effect.void,
}),
)
const auth = Layer.succeed(
Auth.Service,
Auth.Service.of({
get: (id) => Ref.get(records).pipe(Effect.map((items) => items[id])),
all: () => Ref.get(records),
set: (id, value) => Ref.update(records, (items) => ({ ...items, [id]: value })),
remove: (id) =>
Ref.update(records, (items) => Object.fromEntries(Object.entries(items).filter(([key]) => key !== id))),
}),
)
const cache = Layer.mock(ModelCache.Service)({
clear: (id) => Ref.update(events, (items) => [...items, `clear:${id}`]),
})
const instances = Layer.mock(InstanceStore.Service)({
disposeAll: () => Ref.update(events, (items) => [...items, "dispose"]),
})
const layer = Desktop.layer.pipe(
Layer.provide(discovery),
Layer.provide(platform),
Layer.provide(auth),
Layer.provide(cache),
Layer.provide(instances),
)
const first = yield* Desktop.Service.use((service) => service.sync()).pipe(Effect.provide(layer))
expect(first.serverID).toBe("first")
const unchanged = yield* Desktop.Service.use((service) => service.sync()).pipe(Effect.provide(layer))
expect(unchanged.serverID).toBe("first")
expect(yield* Ref.get(events)).toEqual([`clear:${PROVIDER_ID}`, "dispose"])
yield* Ref.set(index, 1)
const second = yield* Desktop.Service.use((service) => service.sync()).pipe(Effect.provide(layer))
expect(second.serverID).toBe("second")
const stored = (yield* Ref.get(records))[PROVIDER_ID]
expect(stored?.type).toBe("api")
if (stored?.type !== "api") throw new Error("standard API auth record was not stored")
expect(stored.key).toBe("second-fixture-key")
const metadata = decodeMetadata(stored.metadata)
expect(metadata?.serverID).toBe("second")
expect(metadata?.baseURL).toBe("http://127.0.0.1:8081/v1")
expect(yield* Ref.get(events)).toEqual([`clear:${PROVIDER_ID}`, "dispose", `clear:${PROVIDER_ID}`, "dispose"])
}),
)
it.live("sync requires acknowledgement for limited tool support", () =>
Effect.gen(function* () {
const writes = yield* Ref.make(0)
const discovery = Layer.succeed(
Discovery.Service,
Discovery.Service.of({ discover: () => Effect.succeed(ready("limited", 8080, "fixture-key", "unknown")) }),
)
const platform = Layer.mock(DesktopPlatform.Service)({
info: { platform: "linux", arch: "x64", home: "/tmp", env: {} },
open: () => Effect.void,
})
const auth = Layer.mock(Auth.Service)({
get: () => Effect.succeed(undefined),
set: () => Ref.update(writes, (count) => count + 1),
})
const cache = Layer.mock(ModelCache.Service)({ clear: () => Effect.void })
const instances = Layer.mock(InstanceStore.Service)({ disposeAll: () => Effect.void })
const layer = Desktop.layer.pipe(
Layer.provide(discovery),
Layer.provide(platform),
Layer.provide(auth),
Layer.provide(cache),
Layer.provide(instances),
)
const refused = yield* Desktop.Service.use((service) => service.sync()).pipe(Effect.provide(layer), Effect.result)
expect(refused._tag).toBe("Failure")
expect(yield* Ref.get(writes)).toBe(0)
const accepted = yield* Desktop.Service.use((service) => service.sync(true)).pipe(Effect.provide(layer))
expect(accepted.serverID).toBe("limited")
expect(yield* Ref.get(writes)).toBe(1)
}),
)
@@ -0,0 +1,124 @@
import { describe, expect, test } from "bun:test"
import type { AnacondaDesktopStatus } from "@kilocode/sdk/v2"
import { createSetupController, type ReadyStatus } from "../../../../src/kilocode/anaconda-desktop/tui/model"
const ready = (toolcall: ReadyStatus["toolcall"] = "supported"): ReadyStatus => ({
type: "ready",
serverID: "server-1",
models: [{ id: "model-1", name: "Local Model" }],
context: 8192,
toolcall,
})
const waiting: AnacondaDesktopStatus = { type: "no-running-server", downloadedModels: 1 }
function deferred<T>() {
let resolve!: (value: T) => void
const promise = new Promise<T>((done) => {
resolve = done
})
return { promise, resolve }
}
async function flush() {
for (const _ of Array.from({ length: 4 })) await Promise.resolve()
}
function setup(input: {
status(signal: AbortSignal): Promise<AnacondaDesktopStatus>
open?(signal: AbortSignal): Promise<void>
sync?(acknowledge: boolean, signal: AbortSignal): Promise<ReadyStatus>
}) {
return createSetupController({
api: {
status: input.status,
open: input.open ?? (async () => {}),
sync: input.sync ?? (async () => ready()),
},
synced: () => {},
})
}
describe("Anaconda Desktop TUI setup controller", () => {
test("checks once on start and only checks again on explicit refresh", async () => {
const calls: Array<ReturnType<typeof deferred<AnacondaDesktopStatus>>> = []
const controller = setup({
status: async () => {
const call = deferred<AnacondaDesktopStatus>()
calls.push(call)
return call.promise
},
})
controller.start()
expect(calls).toHaveLength(1)
expect(await controller.refresh()).toBe(false)
calls[0].resolve(waiting)
await flush()
expect(calls).toHaveLength(1)
const refreshed = controller.refresh()
expect(calls).toHaveLength(2)
calls[1].resolve(waiting)
expect(await refreshed).toBe(true)
controller.stop()
})
test("stops an in-flight check without applying its result", async () => {
const pending = deferred<AnacondaDesktopStatus>()
let signal: AbortSignal | undefined
const controller = setup({
status: (current) => {
signal = current
return pending.promise
},
})
controller.start()
controller.stop()
expect(signal?.aborted).toBe(true)
pending.resolve(waiting)
await flush()
expect(controller.snapshot().status).toBeUndefined()
})
test("opening Desktop does not check status again", async () => {
let checks = 0
let opens = 0
const controller = setup({
status: async () => {
checks += 1
return waiting
},
open: async () => {
opens += 1
},
})
controller.start()
await flush()
await controller.open()
expect({ checks, opens }).toEqual({ checks: 1, opens: 1 })
controller.stop()
})
test("connects explicitly with the correct tool acknowledgement", async () => {
for (const toolcall of ["supported", "unsupported", "unknown"] as const) {
const acknowledgements: boolean[] = []
const controller = setup({
status: async () => ready(toolcall),
sync: async (acknowledge) => {
acknowledgements.push(acknowledge)
return ready(toolcall)
},
})
controller.start()
await flush()
expect(acknowledgements).toEqual([])
await controller.connect()
expect(acknowledgements).toEqual([toolcall !== "supported"])
controller.stop()
}
})
})
@@ -0,0 +1,42 @@
import { expect, test } from "bun:test"
import type { AnacondaDesktopStatus } from "@kilocode/sdk/v2"
import { DOWNLOAD_URL } from "../../../../src/kilocode/anaconda-desktop/domain"
import { setupView } from "../../../../src/kilocode/anaconda-desktop/tui/model"
const ready = (toolcall: "supported" | "unsupported" | "unknown" = "supported"): AnacondaDesktopStatus => ({
type: "ready",
serverID: "server-1",
models: [{ id: "model-1", name: "Local Model" }],
context: 8192,
toolcall,
})
test("maps setup states to their consequential actions", () => {
const cases: Array<{ status: AnacondaDesktopStatus; action?: "download" | "open" | "connect" }> = [
{ status: { type: "unsupported-platform", platform: "freebsd" } },
{ status: { type: "not-installed", downloadURL: DOWNLOAD_URL }, action: "download" },
{ status: { type: "not-running" }, action: "open" },
{ status: { type: "invalid-config", reason: "missing-key" }, action: "open" },
{ status: { type: "signed-out" }, action: "open" },
{ status: { type: "management-unauthorized" }, action: "open" },
{ status: { type: "management-unavailable", reason: "timeout" }, action: "open" },
{ status: { type: "no-downloaded-model" }, action: "open" },
{ status: { type: "no-running-server", downloadedModels: 2 }, action: "open" },
{ status: { type: "inference-unhealthy", serverID: "server-1" }, action: "open" },
{ status: ready(), action: "connect" },
]
for (const item of cases) {
const view = setupView(item.status)
expect(view.actions.at(-1)?.type).toBe("refresh")
expect(view.actions.some((action) => action.type === item.action)).toBe(item.action !== undefined)
}
})
test("requires explicit continuation for limited tool support", () => {
for (const toolcall of ["unsupported", "unknown"] as const) {
const view = setupView(ready(toolcall))
expect(view.warning).toBe(true)
expect(view.actions.find((action) => action.type === "connect")?.label).toBe("connect anyway")
}
})
+118
View File
@@ -8,6 +8,12 @@ import type {
AgentBuilderSaveErrors,
AgentBuilderSaveResponses,
AgentPartInput,
AnacondaDesktopOpenErrors,
AnacondaDesktopOpenResponses,
AnacondaDesktopStatusErrors,
AnacondaDesktopStatusResponses,
AnacondaDesktopSyncErrors,
AnacondaDesktopSyncResponses,
AppAgentsErrors,
AppAgentsResponses,
AppLogErrors,
@@ -7572,6 +7578,113 @@ export class Kilocode extends HeyApiClient {
}
}
export class AnacondaDesktop extends HeyApiClient {
/**
* Get Anaconda Desktop setup status
*
* Discover the locally installed Anaconda Desktop and its active inference server.
*/
public status<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).get<
AnacondaDesktopStatusResponses,
AnacondaDesktopStatusErrors,
ThrowOnError
>({
url: "/kilocode/anaconda-desktop/status",
...options,
...params,
})
}
/**
* Open Anaconda Desktop
*
* Open the locally installed Anaconda Desktop application.
*/
public open<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).post<AnacondaDesktopOpenResponses, AnacondaDesktopOpenErrors, ThrowOnError>(
{
url: "/kilocode/anaconda-desktop/open",
...options,
...params,
},
)
}
/**
* Synchronize Anaconda Desktop provider
*
* Discover the active local inference server and replace Kilo provider authentication metadata.
*/
public sync<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
acknowledgeToolLimitations?: boolean
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
{ in: "body", key: "acknowledgeToolLimitations" },
],
},
],
)
return (options?.client ?? this.client).post<AnacondaDesktopSyncResponses, AnacondaDesktopSyncErrors, ThrowOnError>(
{
url: "/kilocode/anaconda-desktop/sync",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
},
)
}
}
export class Network extends HeyApiClient {
/**
* List pending network waits
@@ -8189,6 +8302,11 @@ export class KiloClient extends HeyApiClient {
return (this._kilocode ??= new Kilocode({ client: this.client }))
}
private _anacondaDesktop?: AnacondaDesktop
get anacondaDesktop(): AnacondaDesktop {
return (this._anacondaDesktop ??= new AnacondaDesktop({ client: this.client }))
}
private _network?: Network
get network(): Network {
return (this._network ??= new Network({ client: this.client }))
+172
View File
@@ -2587,6 +2587,66 @@ export type NotebookFailure = {
currentRevision?: string
}
export type AnacondaDesktopStatus =
| {
type: "unsupported-platform"
platform: string
}
| {
type: "not-installed"
downloadURL: string
}
| {
type: "not-running"
}
| {
type: "invalid-config"
reason: "missing" | "malformed" | "missing-key" | "invalid-port"
}
| {
type: "signed-out"
}
| {
type: "management-unauthorized"
}
| {
type: "management-unavailable"
reason: "timeout" | "unexpected-response"
}
| {
type: "no-downloaded-model"
}
| {
type: "no-running-server"
downloadedModels: number
}
| {
type: "inference-unhealthy"
serverID: string
}
| {
type: "ready"
serverID: string
serverName?: string
models: Array<{
id: string
name: string
}>
context: number
toolcall: "supported" | "unsupported" | "unknown"
}
export type AnacondaDesktopConflictError = {
code: "unsupported-platform" | "not-installed" | "not-ready" | "acknowledgement-required"
message: string
status?: AnacondaDesktopStatus
}
export type AnacondaDesktopOperationError = {
operation: "open" | "sync"
message: string
}
export type KilocodeSessionImportResult = {
ok: boolean
id: string
@@ -10827,6 +10887,118 @@ export type KilocodeNotebookRejectResponses = {
export type KilocodeNotebookRejectResponse = KilocodeNotebookRejectResponses[keyof KilocodeNotebookRejectResponses]
export type AnacondaDesktopStatusData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/kilocode/anaconda-desktop/status"
}
export type AnacondaDesktopStatusErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type AnacondaDesktopStatusError = AnacondaDesktopStatusErrors[keyof AnacondaDesktopStatusErrors]
export type AnacondaDesktopStatusResponses = {
/**
* Anaconda Desktop setup status
*/
200: AnacondaDesktopStatus
}
export type AnacondaDesktopStatusResponse = AnacondaDesktopStatusResponses[keyof AnacondaDesktopStatusResponses]
export type AnacondaDesktopOpenData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/kilocode/anaconda-desktop/open"
}
export type AnacondaDesktopOpenErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* AnacondaDesktopConflictError
*/
409: AnacondaDesktopConflictError
/**
* AnacondaDesktopOperationError
*/
500: AnacondaDesktopOperationError
}
export type AnacondaDesktopOpenError = AnacondaDesktopOpenErrors[keyof AnacondaDesktopOpenErrors]
export type AnacondaDesktopOpenResponses = {
/**
* Anaconda Desktop opened
*/
200: true
}
export type AnacondaDesktopOpenResponse = AnacondaDesktopOpenResponses[keyof AnacondaDesktopOpenResponses]
export type AnacondaDesktopSyncData = {
body?: {
acknowledgeToolLimitations?: boolean
}
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/kilocode/anaconda-desktop/sync"
}
export type AnacondaDesktopSyncErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* AnacondaDesktopConflictError
*/
409: AnacondaDesktopConflictError
/**
* AnacondaDesktopOperationError
*/
500: AnacondaDesktopOperationError
}
export type AnacondaDesktopSyncError = AnacondaDesktopSyncErrors[keyof AnacondaDesktopSyncErrors]
export type AnacondaDesktopSyncResponses = {
/**
* Anaconda Desktop connection synchronized
*/
200: {
type: "ready"
serverID: string
serverName?: string
models: Array<{
id: string
name: string
}>
context: number
toolcall: "supported" | "unsupported" | "unknown"
}
}
export type AnacondaDesktopSyncResponse = AnacondaDesktopSyncResponses[keyof AnacondaDesktopSyncResponses]
export type NetworkListData = {
body?: never
path?: never
+481
View File
@@ -15016,6 +15016,270 @@
]
}
},
"/kilocode/anaconda-desktop/status": {
"get": {
"tags": ["anaconda-desktop"],
"operationId": "anacondaDesktop.status",
"parameters": [
{
"name": "directory",
"in": "query",
"schema": {
"type": "string"
},
"required": false
},
{
"name": "workspace",
"in": "query",
"schema": {
"type": "string"
},
"required": false
}
],
"responses": {
"200": {
"description": "Anaconda Desktop setup status",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AnacondaDesktopStatus"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"description": "Discover the locally installed Anaconda Desktop and its active inference server.",
"summary": "Get Anaconda Desktop setup status",
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.anacondaDesktop.status({\n ...\n})"
}
]
}
},
"/kilocode/anaconda-desktop/open": {
"post": {
"tags": ["anaconda-desktop"],
"operationId": "anacondaDesktop.open",
"parameters": [
{
"name": "directory",
"in": "query",
"schema": {
"type": "string"
},
"required": false
},
{
"name": "workspace",
"in": "query",
"schema": {
"type": "string"
},
"required": false
}
],
"responses": {
"200": {
"description": "Anaconda Desktop opened",
"content": {
"application/json": {
"schema": {
"type": "boolean",
"enum": [true],
"description": "Anaconda Desktop opened"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"409": {
"description": "AnacondaDesktopConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AnacondaDesktopConflictError"
}
}
}
},
"500": {
"description": "AnacondaDesktopOperationError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AnacondaDesktopOperationError"
}
}
}
}
},
"description": "Open the locally installed Anaconda Desktop application.",
"summary": "Open Anaconda Desktop",
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.anacondaDesktop.open({\n ...\n})"
}
]
}
},
"/kilocode/anaconda-desktop/sync": {
"post": {
"tags": ["anaconda-desktop"],
"operationId": "anacondaDesktop.sync",
"parameters": [
{
"name": "directory",
"in": "query",
"schema": {
"type": "string"
},
"required": false
},
{
"name": "workspace",
"in": "query",
"schema": {
"type": "string"
},
"required": false
}
],
"responses": {
"200": {
"description": "Anaconda Desktop connection synchronized",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["ready"]
},
"serverID": {
"type": "string",
"minLength": 1
},
"serverName": {
"type": "string",
"minLength": 1
},
"models": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"name": {
"type": "string",
"minLength": 1
}
},
"required": ["id", "name"],
"additionalProperties": false
},
"minItems": 1
},
"context": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"toolcall": {
"type": "string",
"enum": ["supported", "unsupported", "unknown"]
}
},
"required": ["type", "serverID", "models", "context", "toolcall"],
"additionalProperties": false,
"description": "Anaconda Desktop connection synchronized"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"409": {
"description": "AnacondaDesktopConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AnacondaDesktopConflictError"
}
}
}
},
"500": {
"description": "AnacondaDesktopOperationError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AnacondaDesktopOperationError"
}
}
}
}
},
"description": "Discover the active local inference server and replace Kilo provider authentication metadata.",
"summary": "Synchronize Anaconda Desktop provider",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"acknowledgeToolLimitations": {
"type": "boolean"
}
},
"additionalProperties": false
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.anacondaDesktop.sync({\n ...\n})"
}
]
}
},
"/network": {
"get": {
"tags": ["network"],
@@ -24630,6 +24894,219 @@
"required": ["code", "message"],
"additionalProperties": false
},
"AnacondaDesktopStatus": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["unsupported-platform"]
},
"platform": {
"type": "string"
}
},
"required": ["type", "platform"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["not-installed"]
},
"downloadURL": {
"type": "string"
}
},
"required": ["type", "downloadURL"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["not-running"]
}
},
"required": ["type"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["invalid-config"]
},
"reason": {
"type": "string",
"enum": ["missing", "malformed", "missing-key", "invalid-port"]
}
},
"required": ["type", "reason"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["signed-out"]
}
},
"required": ["type"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["management-unauthorized"]
}
},
"required": ["type"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["management-unavailable"]
},
"reason": {
"type": "string",
"enum": ["timeout", "unexpected-response"]
}
},
"required": ["type", "reason"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["no-downloaded-model"]
}
},
"required": ["type"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["no-running-server"]
},
"downloadedModels": {
"type": "integer"
}
},
"required": ["type", "downloadedModels"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["inference-unhealthy"]
},
"serverID": {
"type": "string",
"minLength": 1
}
},
"required": ["type", "serverID"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["ready"]
},
"serverID": {
"type": "string",
"minLength": 1
},
"serverName": {
"type": "string",
"minLength": 1
},
"models": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"name": {
"type": "string",
"minLength": 1
}
},
"required": ["id", "name"],
"additionalProperties": false
},
"minItems": 1
},
"context": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"toolcall": {
"type": "string",
"enum": ["supported", "unsupported", "unknown"]
}
},
"required": ["type", "serverID", "models", "context", "toolcall"],
"additionalProperties": false
}
]
},
"AnacondaDesktopConflictError": {
"type": "object",
"properties": {
"code": {
"type": "string",
"enum": ["unsupported-platform", "not-installed", "not-ready", "acknowledgement-required"]
},
"message": {
"type": "string"
},
"status": {
"$ref": "#/components/schemas/AnacondaDesktopStatus"
}
},
"required": ["code", "message"],
"additionalProperties": false
},
"AnacondaDesktopOperationError": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["open", "sync"]
},
"message": {
"type": "string"
}
},
"required": ["operation", "message"],
"additionalProperties": false
},
"KilocodeSessionImportResult": {
"type": "object",
"properties": {
@@ -31244,6 +31721,10 @@
"name": "kilocode",
"description": "Kilo-specific routes."
},
{
"name": "anaconda-desktop",
"description": "Local Anaconda Desktop provider setup routes."
},
{
"name": "network",
"description": "Kilo network routes."