mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-31 01:37:28 +08:00
Merge branch 'main' into docs/custom-model-modalities
This commit is contained in:
@@ -20,6 +20,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang
|
||||
- **Source links**: After adding or changing URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`, run `bun run script/extract-source-links.ts` from the repo root and commit the updated `packages/kilo-docs/source-links.md`. CI runs this check — the build fails if the file is stale.
|
||||
- **kilocode_change check**: `bun run check-kilocode-change` from `packages/kilo-vscode/`. CI runs this — `kilocode_change` is a marker for upstream merge conflicts and must not appear in `packages/kilo-vscode/` or `packages/kilo-ui/` (these are entirely Kilo Code additions). Remove the markers before pushing.
|
||||
- **opencode annotation check**: `bun run script/check-opencode-annotations.ts` from repo root. CI runs this on PRs touching `packages/opencode/` — every Kilo-specific change in shared opencode files must be annotated with `kilocode_change` markers. Exempt paths (no markers needed): `packages/opencode/src/kilocode/`, `packages/opencode/test/kilocode/`, and any path containing `kilocode` in the name.
|
||||
- **Backend/SDK programmatic testing**: see [TESTING.md](./TESTING.md) for spawning the local main-branch backend (`bun dev serve`) and driving it via `curl` — use this instead of `kilo serve` (prod binary) when testing backend fixes.
|
||||
|
||||
## Products
|
||||
|
||||
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
# TESTING.md
|
||||
|
||||
How to spin up the **local main-branch** Kilo backend and test it with `curl` / `fetch`. Aimed at a running Kilo CLI agent iterating on backend fixes without rebuilding the VS Code extension, TUI, or desktop app.
|
||||
|
||||
All examples use plain shell + `curl`. Writing TypeScript files is a last resort (see Section 8).
|
||||
|
||||
## TL;DR
|
||||
|
||||
```bash
|
||||
# From repo root. Starts the LOCAL main-branch backend in the background.
|
||||
PASS=$(openssl rand -hex 16)
|
||||
KILO_SERVER_PASSWORD="$PASS" bun dev serve --port 0 >/tmp/kilo-serve.log 2>&1 &
|
||||
echo $! >/tmp/kilo-serve.pid
|
||||
while ! grep -q "kilo server listening" /tmp/kilo-serve.log 2>/dev/null; do sleep 0.1; done
|
||||
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/kilo-serve.log | grep -oE "[0-9]+$")
|
||||
AUTH="Authorization: Basic $(printf 'kilo:%s' "$PASS" | base64 | tr -d '\n')"
|
||||
BASE="http://127.0.0.1:$PORT"
|
||||
|
||||
# Call any endpoint
|
||||
curl -sS -H "$AUTH" -H "x-kilo-directory: $PWD" "$BASE/global/health"
|
||||
|
||||
# Stop when done
|
||||
kill "$(cat /tmp/kilo-serve.pid)" 2>/dev/null || true
|
||||
rm -f /tmp/kilo-serve.pid /tmp/kilo-serve.log
|
||||
```
|
||||
|
||||
## 1. What this doc is for
|
||||
|
||||
Testing local backend fixes against a real running server, talking to it over HTTP the same way the VS Code extension, TUI, and `kilo run --attach` do — but without any of those clients. Every request is a `curl` the agent can copy-paste.
|
||||
|
||||
For in-process tests (no socket, fastest loop) see `packages/opencode/test/kilocode/server/permission-allow-everything.test.ts` for the `Server.Default().app.request(...)` pattern. That's the right tool inside the `packages/opencode/` test suite; this doc is for out-of-process HTTP testing.
|
||||
|
||||
## 2. `kilo serve` vs `bun dev serve` — important
|
||||
|
||||
| Command | What it runs |
|
||||
|---|---|
|
||||
| `kilo serve` | The npm-installed production CLI on `$PATH`. **Not the code in this repo.** |
|
||||
| `bun dev serve …` (repo root) | The local main-branch backend from this worktree. **This is what you want.** |
|
||||
| `bun run --cwd packages/opencode --conditions=browser src/index.ts serve …` | Same as `bun dev serve`, fully expanded. |
|
||||
|
||||
Root `package.json` defines `"dev"` as the full `bun run --cwd packages/opencode --conditions=browser src/index.ts` invocation, so `bun dev <args>` forwards `<args>` to the local CLI entry point (`packages/opencode/src/index.ts`) without touching the installed binary.
|
||||
|
||||
`bun dev` imports the source directly — no rebuild is needed between code edits. Just kill the running server and relaunch.
|
||||
|
||||
Do **not** use `createKiloServer()` from `@kilocode/sdk/v2` to test local code: it spawns the PATH `kilo` binary (`packages/sdk/js/src/v2/server.ts:38-136`), which is the wrong tool here.
|
||||
|
||||
## 3. Starting the backend (background)
|
||||
|
||||
### Random port (recommended)
|
||||
|
||||
```bash
|
||||
KILO_SERVER_PASSWORD=$(openssl rand -hex 16) \
|
||||
bun dev serve --port 0 >/tmp/kilo-serve.log 2>&1 &
|
||||
echo $! >/tmp/kilo-serve.pid
|
||||
while ! grep -q "kilo server listening" /tmp/kilo-serve.log; do sleep 0.1; done
|
||||
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/kilo-serve.log | grep -oE "[0-9]+$")
|
||||
```
|
||||
|
||||
### Fixed port (if you need a stable URL)
|
||||
|
||||
```bash
|
||||
KILO_SERVER_PASSWORD=secret \
|
||||
bun dev serve --port 4096 --hostname 127.0.0.1 \
|
||||
>/tmp/kilo-serve.log 2>&1 &
|
||||
echo $! >/tmp/kilo-serve.pid
|
||||
while ! grep -q "kilo server listening" /tmp/kilo-serve.log; do sleep 0.1; done
|
||||
PORT=4096
|
||||
```
|
||||
|
||||
### No-auth quickstart (fastest)
|
||||
|
||||
Omit `KILO_SERVER_PASSWORD` entirely. The server prints `Warning: KILO_SERVER_PASSWORD is not set; server is unsecured.` and the auth middleware is bypassed — fine for throwaway local testing, never for anything else.
|
||||
|
||||
```bash
|
||||
bun dev serve --port 0 >/tmp/kilo-serve.log 2>&1 &
|
||||
echo $! >/tmp/kilo-serve.pid
|
||||
while ! grep -q "kilo server listening" /tmp/kilo-serve.log; do sleep 0.1; done
|
||||
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/kilo-serve.log | grep -oE "[0-9]+$")
|
||||
BASE="http://127.0.0.1:$PORT"
|
||||
# no AUTH var needed
|
||||
```
|
||||
|
||||
### Flags (`packages/opencode/src/cli/network.ts`)
|
||||
|
||||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--port` | `0` (OS-assigned) | Must be passed literally when overriding `opencode.json`'s `server.port`. |
|
||||
| `--hostname` | `127.0.0.1` | Becomes `0.0.0.0` when `--mdns` is set without an override. |
|
||||
| `--mdns` | `false` | Publishes an mDNS SRV record. |
|
||||
| `--mdns-domain` | `kilo.local` | |
|
||||
| `--cors` | `[]` | Extra allowed origins. |
|
||||
|
||||
## 4. The two mandatory request knobs
|
||||
|
||||
### Auth header (only if `KILO_SERVER_PASSWORD` was set)
|
||||
|
||||
```bash
|
||||
AUTH="Authorization: Basic $(printf 'kilo:%s' "$KILO_SERVER_PASSWORD" | base64 | tr -d '\n')"
|
||||
```
|
||||
|
||||
The username is literally `kilo` (same as the VS Code extension). Skip this whole block if you launched without a password.
|
||||
|
||||
### Directory header on every call
|
||||
|
||||
```bash
|
||||
DIR_HEADER="x-kilo-directory: $PWD"
|
||||
```
|
||||
|
||||
`InstanceMiddleware` uses this to scope the request to a project. For `GET` / `HEAD`, pass `?directory=<urlencoded>` in the URL instead — that's what the SDK does internally.
|
||||
|
||||
## 5. Common `curl` recipes
|
||||
|
||||
All of the below assume `BASE`, `AUTH`, `DIR_HEADER` are set. Drop `-H "$AUTH"` if you're running without a password.
|
||||
|
||||
### Health check (no auth, no directory)
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE/global/health"
|
||||
```
|
||||
|
||||
### Full endpoint list (OpenAPI spec)
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE/doc" | jq .
|
||||
```
|
||||
|
||||
This is the source of truth — anything not in this doc is discoverable from `/doc` without reading source.
|
||||
|
||||
### Create a session
|
||||
|
||||
```bash
|
||||
SID=$(curl -sS -X POST "$BASE/session" \
|
||||
-H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
|
||||
-d '{}' | jq -r .id)
|
||||
echo "$SID"
|
||||
```
|
||||
|
||||
### List sessions (GET — directory goes in the query)
|
||||
|
||||
```bash
|
||||
curl -sS -H "$AUTH" \
|
||||
"$BASE/session?directory=$(printf %s "$PWD" | jq -sRr @uri)"
|
||||
```
|
||||
|
||||
### Send a message (fire-and-forget)
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "$BASE/session/$SID/prompt_async" \
|
||||
-H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
|
||||
-d '{"parts":[{"type":"text","text":"hello"}]}'
|
||||
```
|
||||
|
||||
### Read messages for a session
|
||||
|
||||
```bash
|
||||
curl -sS -H "$AUTH" \
|
||||
"$BASE/session/$SID/message?directory=$(printf %s "$PWD" | jq -sRr @uri)"
|
||||
```
|
||||
|
||||
### Abort an in-flight prompt
|
||||
|
||||
```bash
|
||||
curl -sS -X POST -H "$AUTH" -H "$DIR_HEADER" "$BASE/session/$SID/abort"
|
||||
```
|
||||
|
||||
### Get the resolved config (verify a config change took effect)
|
||||
|
||||
```bash
|
||||
curl -sS -H "$AUTH" \
|
||||
"$BASE/config?directory=$(printf %s "$PWD" | jq -sRr @uri)"
|
||||
```
|
||||
|
||||
### Stream global events (SSE)
|
||||
|
||||
```bash
|
||||
curl -N -sS -H "$AUTH" \
|
||||
"$BASE/global/event?directory=$(printf %s "$PWD" | jq -sRr @uri)"
|
||||
```
|
||||
|
||||
`-N` disables curl's output buffering so events appear live. Expect lines like `data: {"directory":"…","payload":{"type":"…",…}}`.
|
||||
|
||||
## 6. Stopping the backend
|
||||
|
||||
```bash
|
||||
kill "$(cat /tmp/kilo-serve.pid)" 2>/dev/null || true
|
||||
rm -f /tmp/kilo-serve.pid /tmp/kilo-serve.log
|
||||
```
|
||||
|
||||
`ServeCommand` handles `SIGTERM` / `SIGINT` / `SIGHUP` and runs `Instance.disposeAll()` + `server.stop(true)` before exiting (`packages/opencode/src/cli/cmd/serve.ts:29-31`). `-9` is only needed if the process hangs past ~5 s.
|
||||
|
||||
## 7. Useful environment variables
|
||||
|
||||
| Var | Why you'd set it |
|
||||
|---|---|
|
||||
| `KILO_SERVER_PASSWORD` | Enable Basic auth. Omit for auth-bypassed local testing. |
|
||||
| `KILO_DB=":memory:"` | Skip on-disk SQLite — hermetic runs. |
|
||||
| `KILO_DISABLE_DEFAULT_PLUGINS=true` | Don't auto-load bundled plugins. |
|
||||
| `KILO_WORKSPACE_ID=<id>` | Single-workspace mode; disables control-plane routes. |
|
||||
| `KILO_TELEMETRY_LEVEL=off` | Disable PostHog during tests. |
|
||||
| `KILO_CONFIG_CONTENT='{…}'` | Inline JSON config without writing a file. |
|
||||
|
||||
## 8. Last resort: typed SDK via a throwaway script
|
||||
|
||||
Use this only when `curl` can't express what you need — typed request/response shapes, complex multi-turn orchestration, SSE consumers that need to coalesce events. Keep the file short and delete it after.
|
||||
|
||||
```ts
|
||||
// /tmp/probe.ts — delete after use. Talks to an already-running backend.
|
||||
import { createKiloClient } from "@kilocode/sdk/v2"
|
||||
|
||||
const port = process.env.PORT!
|
||||
const pass = process.env.KILO_SERVER_PASSWORD
|
||||
const headers = pass
|
||||
? { Authorization: "Basic " + Buffer.from("kilo:" + pass).toString("base64") }
|
||||
: undefined
|
||||
|
||||
const client = createKiloClient({
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
headers,
|
||||
directory: process.cwd(),
|
||||
})
|
||||
|
||||
const { data: session } = await client.session.create({}, { throwOnError: true })
|
||||
await client.session.promptAsync({
|
||||
sessionID: session.id,
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
})
|
||||
|
||||
const events = await client.global.event({})
|
||||
for await (const ev of events.stream) {
|
||||
const e = ev as { payload: { type: string } }
|
||||
console.log(e.payload.type)
|
||||
if (e.payload.type === "session.idle") break
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
PORT="$PORT" KILO_SERVER_PASSWORD="$KILO_SERVER_PASSWORD" bun /tmp/probe.ts
|
||||
rm /tmp/probe.ts
|
||||
```
|
||||
|
||||
Reminder: this script **connects to** the server you launched in Section 3 — it does not start one. `createKiloServer()` from the SDK would spawn the PATH `kilo` binary (production CLI), which defeats the point of testing local code.
|
||||
|
||||
## 9. Pitfalls
|
||||
|
||||
- Running `kilo serve` instead of `bun dev serve` runs the installed prod binary, not your edits.
|
||||
- Missing `x-kilo-directory` (or `?directory=`) returns `400` from `InstanceMiddleware`.
|
||||
- `curl` without `-N` buffers SSE output — you won't see events until the connection closes.
|
||||
- Hardcoding port `4096` breaks when a previous run didn't exit cleanly. Parse the log instead.
|
||||
- `--port` must appear literally in `argv` to override `opencode.json`'s `server.port` (`packages/opencode/src/cli/network.ts:45`).
|
||||
- `KILO_SERVER_PASSWORD` must be set **before** launch — changing it after doesn't rotate credentials.
|
||||
- When sharing `/tmp/kilo-serve.log` / `.pid` across terminals, unique-suffix the paths to avoid clobbering parallel runs.
|
||||
|
||||
## 10. After changing server routes
|
||||
|
||||
Regenerate the SDK and OpenAPI spec so `/doc` and typed clients stay in sync:
|
||||
|
||||
```bash
|
||||
./script/generate.ts # from repo root
|
||||
```
|
||||
|
||||
See `AGENTS.md` for the full rationale.
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-i18n": "workspace:*",
|
||||
"@kilocode/kilo-ui": "workspace:*",
|
||||
@@ -88,7 +88,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -121,7 +121,7 @@
|
||||
},
|
||||
"packages/desktop-electron": {
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -129,6 +129,7 @@
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "0.15.4",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"electron-context-menu": "4.1.2",
|
||||
"electron-log": "^5",
|
||||
@@ -152,7 +153,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@valibot/to-json-schema": "1.6.0",
|
||||
"electron": "40.8.5",
|
||||
"electron": "41.2.1",
|
||||
"electron-builder": "^26",
|
||||
"electron-vite": "^5",
|
||||
"solid-js": "catalog:",
|
||||
@@ -172,7 +173,7 @@
|
||||
},
|
||||
"packages/kilo-docs": {
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"dependencies": {
|
||||
"@docsearch/css": "^4",
|
||||
"@docsearch/js": "^4",
|
||||
@@ -201,7 +202,7 @@
|
||||
},
|
||||
"packages/kilo-gateway": {
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
@@ -210,7 +211,7 @@
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||
"@openrouter/ai-sdk-provider": "2.8.1",
|
||||
"ai": "catalog:",
|
||||
"open": "10.1.2",
|
||||
"zod": "catalog:",
|
||||
@@ -237,7 +238,7 @@
|
||||
},
|
||||
"packages/kilo-i18n": {
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -250,7 +251,7 @@
|
||||
},
|
||||
"packages/kilo-telemetry": {
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
@@ -270,7 +271,7 @@
|
||||
},
|
||||
"packages/kilo-ui": {
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@opencode-ai/shared": "workspace:*",
|
||||
@@ -305,7 +306,7 @@
|
||||
},
|
||||
"packages/kilo-vscode": {
|
||||
"name": "kilo-code",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@kilocode/kilo-i18n": "workspace:*",
|
||||
@@ -365,7 +366,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@kilocode/cli",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"bin": {
|
||||
"kilo": "./bin/kilo",
|
||||
"kilocode": "./bin/kilo",
|
||||
@@ -412,18 +413,19 @@
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@morphllm/morphsdk": "0.2.166",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@npmcli/config": "10.8.1",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||
"@openrouter/ai-sdk-provider": "2.8.1",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/context-async-hooks": "2.6.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.6.1",
|
||||
"@opentelemetry/sdk-trace-node": "2.6.1",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@opentui/core": "0.1.99",
|
||||
"@opentui/solid": "0.1.99",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -520,15 +522,15 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@opentui/core": "0.1.99",
|
||||
"@opentui/solid": "0.1.99",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
@@ -545,7 +547,7 @@
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"dependencies": {
|
||||
"semver": "^7.6.3",
|
||||
},
|
||||
@@ -556,7 +558,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -571,7 +573,7 @@
|
||||
},
|
||||
"packages/shared": {
|
||||
"name": "@opencode-ai/shared",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -595,7 +597,7 @@
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
@@ -618,7 +620,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -677,6 +679,7 @@
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
"stream-chat@9.38.0": "patches/stream-chat@9.38.0.patch",
|
||||
},
|
||||
"overrides": {
|
||||
@@ -716,7 +719,7 @@
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.11",
|
||||
"@types/bun": "1.3.12",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
@@ -1504,6 +1507,8 @@
|
||||
|
||||
"@npmcli/arborist": ["@npmcli/arborist@9.4.0", "", { "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^5.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/map-workspaces": "^5.0.0", "@npmcli/metavuln-calculator": "^9.0.2", "@npmcli/name-from-folder": "^4.0.0", "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/query": "^5.0.0", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", "minimatch": "^10.0.3", "nopt": "^9.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.0", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "pacote": "^21.0.2", "parse-conflict-json": "^5.0.1", "proc-log": "^6.0.0", "proggy": "^4.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", "semver": "^7.3.7", "ssri": "^13.0.0", "treeverse": "^3.0.0", "walk-up-path": "^4.0.0" }, "bin": { "arborist": "bin/index.js" } }, "sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA=="],
|
||||
|
||||
"@npmcli/config": ["@npmcli/config@10.8.1", "", { "dependencies": { "@npmcli/map-workspaces": "^5.0.0", "@npmcli/package-json": "^7.0.0", "ci-info": "^4.0.0", "ini": "^6.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "walk-up-path": "^4.0.0" } }, "sha512-MAYk9IlIGiyC0c9fnjdBSQfIFPZT0g1MfeSiD1UXTq2zJOLX55jS9/sETJHqw/7LN18JjITrhYfgCfapbmZHiQ=="],
|
||||
|
||||
"@npmcli/fs": ["@npmcli/fs@5.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og=="],
|
||||
|
||||
"@npmcli/git": ["@npmcli/git@7.0.2", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", "semver": "^7.3.5", "which": "^6.0.0" } }, "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg=="],
|
||||
@@ -1576,7 +1581,7 @@
|
||||
|
||||
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
|
||||
|
||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="],
|
||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
@@ -2214,7 +2219,7 @@
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
||||
|
||||
"@types/cacache": ["@types/cacache@20.0.1", "", { "dependencies": { "@types/node": "*", "minipass": "*" } }, "sha512-QlKW3AFoFr/hvPHwFHMIVUH/ZCYeetBNou3PCmxu5LaNDvrtBlPJtIA6uhmU9JRt9oxj7IYoqoLcpxtzpPiTcw=="],
|
||||
|
||||
@@ -2698,7 +2703,7 @@
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||
|
||||
"bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
||||
|
||||
@@ -3074,7 +3079,7 @@
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
"electron": ["electron@40.8.5", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-pgTY/VPQKaiU4sTjfU96iyxCXrFm4htVPCMRT4b7q9ijNTRgtLmLvcmzp2G4e7xDrq9p7OLHSmu1rBKFf6Y1/A=="],
|
||||
"electron": ["electron@41.2.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-teeRThiYGTPKf/2yOW7zZA1bhb91KEQ4yLBPOg7GxpmnkLFLugKgQaAKOrCgdzwsXh/5mFIfmkm+4+wACJKwaA=="],
|
||||
|
||||
"electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="],
|
||||
|
||||
@@ -5286,6 +5291,8 @@
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.75", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-V8UKK4fNpI9cnrtsZBvUp9O9J6Y9fTKBRoSLyEaNGPirACewixmLDbXsSgAeownPVWiWpK34bFysd+XouI5Ywg=="],
|
||||
|
||||
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="],
|
||||
|
||||
"ajv-keywords/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
|
||||
"app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1775823930,
|
||||
"narHash": "sha256-ALT447J7FcxP/97J01A/gp/hgdO5lXRsm+zLMt+gIjc=",
|
||||
"lastModified": 1776683584,
|
||||
"narHash": "sha256-NuTLMrr10Tng72hurYG8jYQ4XKK8wnpJmOGcPiis96g=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8c11f88bb9573a10a7d6bf87161ef08455ac70b9",
|
||||
"rev": "9dd5558b06dbdacbf635a3dd36dce1b1a7ee3a89",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-tWXPyyUluVXzMJny376Nly7hJuiGonPRMv1m48hdplY=",
|
||||
"aarch64-linux": "sha256-MFJ/ihYcJC1QdfJvgalo4/deQ+gaGN+c53Ex1P1ExoI=",
|
||||
"aarch64-darwin": "sha256-wZ+gntTCb/Wzq6yur/1QoqWp6xX21A+XCsPn32KemyY=",
|
||||
"x86_64-darwin": "sha256-lBwBWeXR9ca2TKmeE8E4HK4ZQrf83Ks0M0WAGBOGBfM="
|
||||
"x86_64-linux": "sha256-w2yWlMGc9MVtbgLGZ77y7CzlV+FHFEIBMuyY2+SVKzQ=",
|
||||
"aarch64-linux": "sha256-dNczn+1Gdm6MQNsEHiBuWHO9pFg/wkZ8DndRScFllyA=",
|
||||
"aarch64-darwin": "sha256-flKz2Hejb6VL4Ygm2PHMJWTNpPxEWDX4lwZkUqWiggo=",
|
||||
"x86_64-darwin": "sha256-pZmMQD73/t+mPjwKXjQVQ6guyrXZP9X1vsWoxZXdYWs="
|
||||
}
|
||||
}
|
||||
|
||||
+18
-17
@@ -7,6 +7,7 @@
|
||||
sysctl,
|
||||
makeBinaryWrapper,
|
||||
models-dev,
|
||||
ripgrep,
|
||||
installShellFiles,
|
||||
versionCheckHook,
|
||||
writableTmpDirAsHomeHook,
|
||||
@@ -51,25 +52,25 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase =
|
||||
''
|
||||
runHook preInstall
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
install -Dm755 dist/@kilocode/cli-*/bin/kilo $out/bin/kilo
|
||||
install -Dm644 schema.json $out/share/kilo/schema.json
|
||||
''
|
||||
# bun runs sysctl to detect if dunning on rosetta2
|
||||
+ lib.optionalString stdenvNoCC.hostPlatform.isDarwin ''
|
||||
wrapProgram $out/bin/kilo \
|
||||
--prefix PATH : ${
|
||||
lib.makeBinPath [
|
||||
sysctl
|
||||
install -Dm755 dist/@kilocode/cli-*/bin/kilo $out/bin/kilo
|
||||
install -Dm644 schema.json $out/share/kilo/schema.json
|
||||
|
||||
wrapProgram $out/bin/kilo \
|
||||
--prefix PATH : ${
|
||||
lib.makeBinPath (
|
||||
[
|
||||
ripgrep
|
||||
]
|
||||
}
|
||||
''
|
||||
+ ''
|
||||
runHook postInstall
|
||||
'';
|
||||
# bun runs sysctl to detect if dunning on rosetta2
|
||||
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
||||
)
|
||||
}
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
||||
# trick yargs into also generating zsh completions
|
||||
|
||||
+5
-4
@@ -4,10 +4,10 @@
|
||||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.11",
|
||||
"packageManager": "bun@1.3.13",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
||||
"dev:desktop": "bun --cwd packages/desktop-electron dev",
|
||||
"dev:web": "bun --cwd packages/app dev",
|
||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
@@ -29,7 +29,7 @@
|
||||
"@effect/opentelemetry": "4.0.0-beta.48",
|
||||
"@effect/platform-node": "4.0.0-beta.48",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.3.11",
|
||||
"@types/bun": "1.3.12",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
@@ -141,10 +141,11 @@
|
||||
"happy-dom": ">=20.8.9"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"stream-chat@9.38.0": "patches/stream-chat@9.38.0.patch"
|
||||
},
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
Binary file not shown.
+21
-21
@@ -141,13 +141,11 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
||||
<LanguageProvider locale={props.locale}>
|
||||
<UiI18nBridge>
|
||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||
<QueryProvider>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</QueryProvider>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</ErrorBoundary>
|
||||
</UiI18nBridge>
|
||||
</LanguageProvider>
|
||||
@@ -293,20 +291,22 @@ export function AppInterface(props: {
|
||||
>
|
||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||
<ServerKey>
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
<Dynamic
|
||||
component={props.router ?? Router}
|
||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||
>
|
||||
<Route path="/" component={HomeRoute} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={SessionIndexRoute} />
|
||||
<Route path="/session/:id?" component={SessionRoute} />
|
||||
</Route>
|
||||
</Dynamic>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
<QueryProvider>
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
<Dynamic
|
||||
component={props.router ?? Router}
|
||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||
>
|
||||
<Route path="/" component={HomeRoute} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={SessionIndexRoute} />
|
||||
<Route path="/session/:id?" component={SessionRoute} />
|
||||
</Route>
|
||||
</Dynamic>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
</QueryProvider>
|
||||
</ServerKey>
|
||||
</ConnectionGate>
|
||||
</ServerProvider>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { getProjectAvatarSource } from "@/pages/layout/sidebar-items"
|
||||
|
||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||
|
||||
@@ -26,8 +27,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
name: defaultName(),
|
||||
color: props.project.icon?.color || "pink",
|
||||
iconUrl: props.project.icon?.override || "",
|
||||
color: props.project.icon?.color,
|
||||
iconOverride: props.project.icon?.override,
|
||||
startup: props.project.commands?.start ?? "",
|
||||
dragOver: false,
|
||||
iconHover: false,
|
||||
@@ -39,7 +40,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
if (!file.type.startsWith("image/")) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
setStore("iconUrl", e.target?.result as string)
|
||||
setStore("iconOverride", e.target?.result as string)
|
||||
setStore("iconHover", false)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
@@ -68,7 +69,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
}
|
||||
|
||||
function clearIcon() {
|
||||
setStore("iconUrl", "")
|
||||
setStore("iconOverride", "")
|
||||
}
|
||||
|
||||
const saveMutation = useMutation(() => ({
|
||||
@@ -81,17 +82,17 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
projectID: props.project.id,
|
||||
directory: props.project.worktree,
|
||||
name,
|
||||
icon: { color: store.color, override: store.iconUrl },
|
||||
icon: { color: store.color || "", override: store.iconOverride || "" },
|
||||
commands: { start },
|
||||
})
|
||||
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
|
||||
globalSync.project.icon(props.project.worktree, store.iconOverride || undefined)
|
||||
dialog.close()
|
||||
return
|
||||
}
|
||||
|
||||
globalSync.project.meta(props.project.worktree, {
|
||||
name,
|
||||
icon: { color: store.color, override: store.iconUrl || undefined },
|
||||
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
|
||||
commands: { start: start || undefined },
|
||||
})
|
||||
dialog.close()
|
||||
@@ -130,13 +131,13 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
classList={{
|
||||
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
|
||||
"border-border-base hover:border-border-strong": !store.dragOver,
|
||||
"overflow-hidden": !!store.iconUrl,
|
||||
"overflow-hidden": !!store.iconOverride,
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={() => {
|
||||
if (store.iconUrl && store.iconHover) {
|
||||
if (store.iconOverride && store.iconHover) {
|
||||
clearIcon()
|
||||
} else {
|
||||
iconInput?.click()
|
||||
@@ -144,7 +145,11 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={store.iconUrl}
|
||||
when={getProjectAvatarSource(props.project.id, {
|
||||
color: store.color,
|
||||
url: props.project.icon?.url,
|
||||
override: store.iconOverride,
|
||||
})}
|
||||
fallback={
|
||||
<div class="size-full flex items-center justify-center">
|
||||
<Avatar
|
||||
@@ -155,18 +160,20 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={store.iconUrl}
|
||||
alt={language.t("dialog.project.edit.icon.alt")}
|
||||
class="size-full object-cover"
|
||||
/>
|
||||
{(src) => (
|
||||
<img
|
||||
src={src()}
|
||||
alt={language.t("dialog.project.edit.icon.alt")}
|
||||
class="size-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||
classList={{
|
||||
"opacity-100": store.iconHover && !store.iconUrl,
|
||||
"opacity-0": !(store.iconHover && !store.iconUrl),
|
||||
"opacity-100": store.iconHover && !store.iconOverride,
|
||||
"opacity-0": !(store.iconHover && !store.iconOverride),
|
||||
}}
|
||||
>
|
||||
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||
@@ -174,8 +181,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
<div
|
||||
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||
classList={{
|
||||
"opacity-100": store.iconHover && !!store.iconUrl,
|
||||
"opacity-0": !(store.iconHover && !!store.iconUrl),
|
||||
"opacity-100": store.iconHover && !!store.iconOverride,
|
||||
"opacity-0": !(store.iconHover && !!store.iconOverride),
|
||||
}}
|
||||
>
|
||||
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||
@@ -198,7 +205,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={!store.iconUrl}>
|
||||
<Show when={!store.iconOverride}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
|
||||
<div class="flex gap-1.5">
|
||||
@@ -215,7 +222,10 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
||||
store.color !== color,
|
||||
}}
|
||||
onClick={() => setStore("color", color)}
|
||||
onClick={() => {
|
||||
if (store.color === color && !props.project.icon?.url) return
|
||||
setStore("color", store.color === color ? undefined : color)
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
fallback={store.name || defaultName()}
|
||||
|
||||
@@ -504,7 +504,7 @@ export function DialogSelectServer() {
|
||||
|
||||
return (
|
||||
<Dialog title={formTitle()}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-1 min-h-0 flex-col gap-2">
|
||||
<Show
|
||||
when={!isFormMode()}
|
||||
fallback={
|
||||
@@ -539,7 +539,7 @@ export function DialogSelectServer() {
|
||||
if (x) void select(x)
|
||||
}}
|
||||
divider={true}
|
||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||
class="flex-1 min-h-0 px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||
>
|
||||
{(i) => {
|
||||
const key = ServerConnection.key(i)
|
||||
@@ -619,7 +619,7 @@ export function DialogSelectServer() {
|
||||
</List>
|
||||
</Show>
|
||||
|
||||
<div class="px-5 pb-5">
|
||||
<div class="shrink-0 px-5 pb-5">
|
||||
<Show
|
||||
when={isFormMode()}
|
||||
fallback={
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
|
||||
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal, createResource } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
||||
@@ -54,7 +54,7 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
||||
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
import { useQueries } from "@tanstack/solid-query"
|
||||
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
||||
|
||||
interface PromptInputProps {
|
||||
@@ -1252,16 +1252,23 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
|
||||
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
|
||||
queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)],
|
||||
}))
|
||||
|
||||
const agentsLoading = () => agentsQuery.isLoading
|
||||
|
||||
const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
|
||||
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
|
||||
|
||||
const agentsShouldFadeIn = createMemo((prev) => prev ?? agentsLoading())
|
||||
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
|
||||
const providersShouldFadeIn = createMemo((prev) => prev ?? providersLoading())
|
||||
|
||||
const [promptReady] = createResource(
|
||||
() => prompt.ready().promise,
|
||||
(p) => p,
|
||||
)
|
||||
|
||||
return (
|
||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
||||
{(promptReady(), null)}
|
||||
<PromptPopover
|
||||
popover={store.popover}
|
||||
setSlashPopoverRef={(el) => (slashPopoverRef = el)}
|
||||
@@ -1359,15 +1366,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}}
|
||||
style={{ "padding-bottom": space }}
|
||||
/>
|
||||
<Show when={!prompt.dirty()}>
|
||||
<div
|
||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||
classList={{ "font-mono!": store.mode === "shell" }}
|
||||
style={{ "padding-bottom": space }}
|
||||
>
|
||||
{placeholder()}
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||
classList={{ "font-mono!": store.mode === "shell" }}
|
||||
style={{ "padding-bottom": space, display: prompt.dirty() ? "none" : undefined }}
|
||||
>
|
||||
{placeholder()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -1461,7 +1466,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||
<Show when={!agentsLoading()}>
|
||||
<div data-component="prompt-agent-control">
|
||||
<div
|
||||
data-component="prompt-agent-control"
|
||||
style={agentsShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
@@ -1487,7 +1495,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</Show>
|
||||
<Show when={!providersLoading()}>
|
||||
<Show when={store.mode !== "shell"}>
|
||||
<div data-component="prompt-model-control">
|
||||
<div
|
||||
data-component="prompt-model-control"
|
||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<Show
|
||||
when={providers.paid().length > 0}
|
||||
fallback={
|
||||
@@ -1558,7 +1569,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
<div data-component="prompt-variant-control">
|
||||
<div
|
||||
data-component="prompt-variant-control"
|
||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
|
||||
@@ -19,6 +19,9 @@ import {
|
||||
sansDefault,
|
||||
sansFontFamily,
|
||||
sansInput,
|
||||
terminalDefault,
|
||||
terminalFontFamily,
|
||||
terminalInput,
|
||||
useSettings,
|
||||
} from "@/context/settings"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
@@ -181,6 +184,7 @@ export const SettingsGeneral: Component = () => {
|
||||
const soundOptions = [noneSound, ...SOUND_OPTIONS]
|
||||
const mono = () => monoInput(settings.appearance.font())
|
||||
const sans = () => sansInput(settings.appearance.uiFont())
|
||||
const terminal = () => terminalInput(settings.appearance.terminalFont())
|
||||
|
||||
const soundSelectProps = (
|
||||
enabled: () => boolean,
|
||||
@@ -276,6 +280,18 @@ export const SettingsGeneral: Component = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.showSessionProgressBar.title")}
|
||||
description={language.t("settings.general.row.showSessionProgressBar.description")}
|
||||
>
|
||||
<div data-action="settings-show-session-progress-bar">
|
||||
<Switch
|
||||
checked={settings.general.showSessionProgressBar()}
|
||||
onChange={(checked) => settings.general.setShowSessionProgressBar(checked)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</SettingsList>
|
||||
</div>
|
||||
)
|
||||
@@ -451,6 +467,29 @@ export const SettingsGeneral: Component = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.terminalFont.title")}
|
||||
description={language.t("settings.general.row.terminalFont.description")}
|
||||
>
|
||||
<div class="w-full sm:w-[220px]">
|
||||
<TextField
|
||||
data-action="settings-terminal-font"
|
||||
label={language.t("settings.general.row.terminalFont.title")}
|
||||
hideLabel
|
||||
type="text"
|
||||
value={terminal()}
|
||||
onChange={(value) => settings.appearance.setTerminalFont(value)}
|
||||
placeholder={terminalDefault}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
class="text-12-regular"
|
||||
style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</SettingsList>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useServer } from "@/context/server"
|
||||
import { monoFontFamily, useSettings } from "@/context/settings"
|
||||
import { terminalFontFamily, useSettings } from "@/context/settings"
|
||||
import type { LocalPTY } from "@/context/terminal"
|
||||
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
||||
import { terminalWriter } from "@/utils/terminal-writer"
|
||||
@@ -300,7 +300,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const font = monoFontFamily(settings.appearance.font())
|
||||
const font = terminalFontFamily(settings.appearance.terminalFont())
|
||||
if (!term) return
|
||||
setOptionIfSupported(term, "fontFamily", font)
|
||||
scheduleFit()
|
||||
@@ -360,7 +360,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
cols: restoreSize?.cols,
|
||||
rows: restoreSize?.rows,
|
||||
fontSize: 14,
|
||||
fontFamily: monoFontFamily(settings.appearance.font()),
|
||||
fontFamily: terminalFontFamily(settings.appearance.terminalFont()),
|
||||
allowTransparency: false,
|
||||
convertEol: false,
|
||||
theme: terminalColors(),
|
||||
|
||||
@@ -9,10 +9,9 @@ import type {
|
||||
} from "@kilocode/sdk/v2/client"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||
import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
||||
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import type { InitError } from "../pages/error"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
|
||||
@@ -24,7 +23,6 @@ import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global
|
||||
import { trimSessions } from "./global-sync/session-trim"
|
||||
import type { ProjectMeta } from "./global-sync/types"
|
||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||
import { sanitizeProject } from "./global-sync/utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
|
||||
|
||||
@@ -56,15 +54,10 @@ function createGlobalSync() {
|
||||
const sessionLoads = new Map<string, Promise<void>>()
|
||||
const sessionMeta = new Map<string, { limit: number }>()
|
||||
|
||||
const [projectCache, setProjectCache, projectInit] = persisted(
|
||||
Persist.global("globalSync.project", ["globalSync.project.v1"]),
|
||||
createStore({ value: [] as Project[] }),
|
||||
)
|
||||
|
||||
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
|
||||
ready: false,
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
project: projectCache.value,
|
||||
project: [],
|
||||
session_todo: {},
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
provider_auth: {},
|
||||
@@ -73,37 +66,18 @@ function createGlobalSync() {
|
||||
})
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
let active = true
|
||||
let projectWritten = false
|
||||
let bootedAt = 0
|
||||
let bootingRoot = false
|
||||
let eventFrame: number | undefined
|
||||
let eventTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
onCleanup(() => {
|
||||
active = false
|
||||
})
|
||||
onCleanup(() => {
|
||||
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
|
||||
if (eventTimer !== undefined) clearTimeout(eventTimer)
|
||||
})
|
||||
|
||||
const cacheProjects = () => {
|
||||
setProjectCache(
|
||||
"value",
|
||||
untrack(() => globalStore.project.map(sanitizeProject)),
|
||||
)
|
||||
}
|
||||
|
||||
const setProjects = (next: Project[] | ((draft: Project[]) => void)) => {
|
||||
projectWritten = true
|
||||
if (typeof next === "function") {
|
||||
setGlobalStore("project", produce(next))
|
||||
cacheProjects()
|
||||
return
|
||||
}
|
||||
const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => {
|
||||
setGlobalStore("project", next)
|
||||
cacheProjects()
|
||||
}
|
||||
|
||||
const setBootStore = ((...input: unknown[]) => {
|
||||
@@ -116,22 +90,12 @@ function createGlobalSync() {
|
||||
|
||||
const set = ((...input: unknown[]) => {
|
||||
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
|
||||
setProjects(input[1] as Project[] | ((draft: Project[]) => void))
|
||||
setProjects(input[1] as Project[] | ((draft: Project[]) => Project[]))
|
||||
return input[1]
|
||||
}
|
||||
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
||||
}) as typeof setGlobalStore
|
||||
|
||||
if (projectInit instanceof Promise) {
|
||||
void projectInit.then(() => {
|
||||
if (!active) return
|
||||
if (projectWritten) return
|
||||
const cached = projectCache.value
|
||||
if (cached.length === 0) return
|
||||
setGlobalStore("project", cached)
|
||||
})
|
||||
}
|
||||
|
||||
const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
|
||||
if (!sessionID) return
|
||||
if (!todos) {
|
||||
@@ -223,16 +187,18 @@ function createGlobalSync() {
|
||||
limit,
|
||||
permission: store.permission,
|
||||
})
|
||||
setStore(
|
||||
"sessionTotal",
|
||||
estimateRootSessionTotal({
|
||||
count: nonArchived.length,
|
||||
limit: x.limit,
|
||||
limited: x.limited,
|
||||
}),
|
||||
)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||
batch(() => {
|
||||
setStore(
|
||||
"sessionTotal",
|
||||
estimateRootSessionTotal({
|
||||
count: nonArchived.length,
|
||||
limit: x.limit,
|
||||
limited: x.limited,
|
||||
}),
|
||||
)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||
})
|
||||
sessionMeta.set(directory, { limit })
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -298,6 +264,19 @@ function createGlobalSync() {
|
||||
const event = e.details
|
||||
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
||||
|
||||
if (event.type === "session.error") {
|
||||
const error = event.properties.error
|
||||
if (error?.name !== "MessageAbortedError") {
|
||||
console.error("[global-sync] session error", {
|
||||
scope: directory === "global" ? "global" : "workspace",
|
||||
directory: directory === "global" ? undefined : directory,
|
||||
project: directory === "global" ? undefined : getFilename(directory),
|
||||
sessionID: event.properties.sessionID,
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (directory === "global") {
|
||||
applyGlobalEvent({
|
||||
event,
|
||||
|
||||
@@ -19,7 +19,6 @@ import type { State, VcsCache } from "./types"
|
||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
|
||||
import { loadSessionsQuery } from "../global-sync"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -82,6 +81,9 @@ export async function bootstrapGlobal(input: {
|
||||
input.setGlobalStore("config", x.data!)
|
||||
}),
|
||||
),
|
||||
]
|
||||
|
||||
const slow = [
|
||||
() =>
|
||||
input.queryClient.fetchQuery({
|
||||
...loadProvidersQuery(null),
|
||||
@@ -93,9 +95,6 @@ export async function bootstrapGlobal(input: {
|
||||
}),
|
||||
),
|
||||
}),
|
||||
]
|
||||
|
||||
const slow = [
|
||||
() =>
|
||||
retry(() =>
|
||||
input.globalSDK.path.get().then((x) => {
|
||||
@@ -183,8 +182,43 @@ function warmSessions(input: {
|
||||
export const loadProvidersQuery = (directory: string | null) =>
|
||||
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
||||
|
||||
export const loadAgentsQuery = (directory: string | null) =>
|
||||
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
|
||||
export const loadAgentsQuery = (
|
||||
directory: string | null,
|
||||
sdk?: KiloClient,
|
||||
transform?: (x: Awaited<ReturnType<KiloClient["app"]["agents"]>>) => void,
|
||||
) =>
|
||||
queryOptions<null>({
|
||||
queryKey: [directory, "agents"],
|
||||
queryFn:
|
||||
sdk && transform
|
||||
? () =>
|
||||
retry(() =>
|
||||
sdk.app
|
||||
.agents()
|
||||
.then(transform)
|
||||
.then(() => null),
|
||||
)
|
||||
: skipToken,
|
||||
})
|
||||
|
||||
export const loadPathQuery = (
|
||||
directory: string | null,
|
||||
sdk?: KiloClient,
|
||||
transform?: (x: Awaited<ReturnType<KiloClient["path"]["get"]>>) => void,
|
||||
) =>
|
||||
queryOptions<Path>({
|
||||
queryKey: [directory, "path"],
|
||||
queryFn:
|
||||
sdk && transform
|
||||
? () =>
|
||||
retry(() =>
|
||||
sdk.path.get().then(async (x) => {
|
||||
transform(x)
|
||||
return x.data!
|
||||
}),
|
||||
)
|
||||
: skipToken,
|
||||
})
|
||||
|
||||
export async function bootstrapDirectory(input: {
|
||||
directory: string
|
||||
@@ -222,45 +256,27 @@ export async function bootstrapDirectory(input: {
|
||||
input.setStore("lsp", [])
|
||||
if (loading) input.setStore("status", "partial")
|
||||
|
||||
const fast = [() => Promise.resolve(input.loadSessions(input.directory))]
|
||||
|
||||
const errs = errors(await runAll(fast))
|
||||
if (errs.length > 0) {
|
||||
console.error("Failed to bootstrap instance", errs[0])
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(errs[0], input.translate),
|
||||
})
|
||||
}
|
||||
|
||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||
providerRev.set(input.directory, rev)
|
||||
;(async () => {
|
||||
const slow = [
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
() =>
|
||||
input.queryClient.ensureQueryData({
|
||||
...loadAgentsQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
|
||||
() => null,
|
||||
),
|
||||
}),
|
||||
input.queryClient.ensureQueryData(
|
||||
loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))),
|
||||
),
|
||||
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||
() =>
|
||||
seededProject
|
||||
? Promise.resolve()
|
||||
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||
() =>
|
||||
seededPath
|
||||
? Promise.resolve()
|
||||
: retry(() =>
|
||||
input.sdk.path.get().then((x) => {
|
||||
input.setStore("path", x.data!)
|
||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||
if (next) input.setStore("project", next)
|
||||
}),
|
||||
),
|
||||
!seededProject &&
|
||||
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
|
||||
!seededPath &&
|
||||
(() =>
|
||||
input.queryClient.ensureQueryData(
|
||||
loadPathQuery(input.directory, input.sdk, (x) => {
|
||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||
if (next) input.setStore("project", next)
|
||||
}),
|
||||
)),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.vcs.get().then((x) => {
|
||||
@@ -330,7 +346,28 @@ export async function bootstrapDirectory(input: {
|
||||
input.setStore("mcp_ready", true)
|
||||
}),
|
||||
),
|
||||
]
|
||||
() =>
|
||||
input.queryClient.ensureQueryData({
|
||||
...loadProvidersQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.provider.list())
|
||||
.then((x) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
input.setStore("provider", normalizeProviderList(x.data!))
|
||||
input.setStore("provider_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
})
|
||||
.then(() => null),
|
||||
}),
|
||||
].filter(Boolean) as (() => Promise<any>)[]
|
||||
|
||||
await waitForPaint()
|
||||
const slowErrs = errors(await runAll(slow))
|
||||
@@ -344,29 +381,6 @@ export async function bootstrapDirectory(input: {
|
||||
})
|
||||
}
|
||||
|
||||
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
||||
|
||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||
providerRev.set(input.directory, rev)
|
||||
void input.queryClient.ensureQueryData({
|
||||
...loadSessionsQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.provider.list())
|
||||
.then((x) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
input.setStore("provider", normalizeProviderList(x.data!))
|
||||
input.setStore("provider_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
})
|
||||
.then(() => null),
|
||||
})
|
||||
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
|
||||
})()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
type VcsCache,
|
||||
} from "./types"
|
||||
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
import { loadPathQuery } from "./bootstrap"
|
||||
|
||||
export function createChildStoreManager(input: {
|
||||
owner: Owner
|
||||
@@ -154,16 +156,21 @@ export function createChildStoreManager(input: {
|
||||
|
||||
const init = () =>
|
||||
createRoot((dispose) => {
|
||||
const initialMeta = meta[0].value
|
||||
const initialIcon = icon[0].value
|
||||
|
||||
const pathQuery = useQuery(() => loadPathQuery(directory))
|
||||
const child = createStore<State>({
|
||||
project: "",
|
||||
projectMeta: initialMeta,
|
||||
projectMeta: undefined,
|
||||
icon: initialIcon,
|
||||
provider_ready: false,
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
get path() {
|
||||
if (pathQuery.isLoading || !pathQuery.data)
|
||||
return { state: "", config: "", worktree: "", directory: "", home: "" }
|
||||
return pathQuery.data
|
||||
},
|
||||
status: "loading" as const,
|
||||
agent: [],
|
||||
command: [],
|
||||
@@ -200,11 +207,6 @@ export function createChildStoreManager(input: {
|
||||
child[1]("vcs", (value) => value ?? cached)
|
||||
})
|
||||
|
||||
onPersistedInit(meta[2], () => {
|
||||
if (child[0].projectMeta !== initialMeta) return
|
||||
child[1]("projectMeta", meta[0].value)
|
||||
})
|
||||
|
||||
onPersistedInit(icon[2], () => {
|
||||
if (child[0].icon !== initialIcon) return
|
||||
child[1]("icon", icon[0].value)
|
||||
|
||||
@@ -21,7 +21,7 @@ const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
export function applyGlobalEvent(input: {
|
||||
event: { type: string; properties?: unknown }
|
||||
project: Project[]
|
||||
setGlobalProject: (next: Project[] | ((draft: Project[]) => void)) => void
|
||||
setGlobalProject: (next: Project[] | ((draft: Project[]) => Project[])) => void
|
||||
refresh: () => void
|
||||
}) {
|
||||
if (input.event.type === "global.disposed" || input.event.type === "server.connected") {
|
||||
@@ -33,14 +33,18 @@ export function applyGlobalEvent(input: {
|
||||
const properties = input.event.properties as Project
|
||||
const result = Binary.search(input.project, properties.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
input.setGlobalProject((draft) => {
|
||||
draft[result.index] = { ...draft[result.index], ...properties }
|
||||
})
|
||||
input.setGlobalProject(
|
||||
produce((draft) => {
|
||||
draft[result.index] = { ...draft[result.index], ...properties }
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
input.setGlobalProject((draft) => {
|
||||
draft.splice(result.index, 0, properties)
|
||||
})
|
||||
input.setGlobalProject(
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, properties)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function cleanupSessionCaches(
|
||||
|
||||
@@ -391,37 +391,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
? globalSync.data.project.find((x) => x.id === projectID)
|
||||
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||
|
||||
const local = childStore.projectMeta
|
||||
const localOverride =
|
||||
local?.name !== undefined ||
|
||||
local?.commands?.start !== undefined ||
|
||||
local?.icon?.override !== undefined ||
|
||||
local?.icon?.color !== undefined
|
||||
|
||||
const base = {
|
||||
...metadata,
|
||||
...project,
|
||||
icon: {
|
||||
url: metadata?.icon?.url,
|
||||
override: metadata?.icon?.override ?? childStore.icon,
|
||||
color: metadata?.icon?.color,
|
||||
},
|
||||
}
|
||||
|
||||
const isGlobal = projectID === "global" || (metadata?.id === undefined && localOverride)
|
||||
if (!isGlobal) return base
|
||||
|
||||
return {
|
||||
...base,
|
||||
id: base.id ?? "global",
|
||||
name: local?.name,
|
||||
commands: local?.commands,
|
||||
icon: {
|
||||
url: base.icon?.url,
|
||||
override: local?.icon?.override,
|
||||
color: local?.icon?.color,
|
||||
},
|
||||
}
|
||||
return { ...metadata, ...project }
|
||||
}
|
||||
|
||||
const roots = createMemo(() => {
|
||||
@@ -516,7 +486,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
|
||||
for (const project of projects) {
|
||||
if (project.icon?.color) continue
|
||||
if (project.icon?.color || project.icon?.override || project.icon?.url) continue
|
||||
const worktree = project.worktree
|
||||
const existing = colors[worktree]
|
||||
const color = existing ?? pickAvailableColor(used)
|
||||
|
||||
@@ -185,9 +185,9 @@ function createPromptSession(dir: string, id: string | undefined) {
|
||||
|
||||
return {
|
||||
ready,
|
||||
current: createMemo(() => store.prompt),
|
||||
current: () => store.prompt,
|
||||
cursor: createMemo(() => store.cursor),
|
||||
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
|
||||
dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT),
|
||||
context: {
|
||||
items: createMemo(() => store.context.items),
|
||||
add(item: ContextItem) {
|
||||
@@ -277,7 +277,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
||||
const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())
|
||||
|
||||
return {
|
||||
ready: () => session().ready(),
|
||||
ready: () => session().ready,
|
||||
current: () => session().current(),
|
||||
cursor: () => session().cursor(),
|
||||
dirty: () => session().dirty(),
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface Settings {
|
||||
showReasoningSummaries: boolean
|
||||
shellToolPartsExpanded: boolean
|
||||
editToolPartsExpanded: boolean
|
||||
showSessionProgressBar: boolean
|
||||
}
|
||||
updates: {
|
||||
startup: boolean
|
||||
@@ -39,6 +40,7 @@ export interface Settings {
|
||||
fontSize: number
|
||||
mono: string
|
||||
sans: string
|
||||
terminal: string
|
||||
}
|
||||
keybinds: Record<string, string>
|
||||
permissions: {
|
||||
@@ -50,13 +52,17 @@ export interface Settings {
|
||||
|
||||
export const monoDefault = "System Mono"
|
||||
export const sansDefault = "System Sans"
|
||||
export const terminalDefault = "JetBrainsMono Nerd Font Mono"
|
||||
|
||||
const monoFallback =
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||
const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
||||
const terminalFallback =
|
||||
'"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||
|
||||
const monoBase = monoFallback
|
||||
const sansBase = sansFallback
|
||||
const terminalBase = terminalFallback
|
||||
|
||||
function input(font: string | undefined) {
|
||||
return font ?? ""
|
||||
@@ -89,6 +95,14 @@ export function sansFontFamily(font: string | undefined) {
|
||||
return stack(font, sansBase)
|
||||
}
|
||||
|
||||
export function terminalInput(font: string | undefined) {
|
||||
return input(font)
|
||||
}
|
||||
|
||||
export function terminalFontFamily(font: string | undefined) {
|
||||
return stack(font, terminalBase)
|
||||
}
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
general: {
|
||||
autoSave: true,
|
||||
@@ -102,6 +116,7 @@ const defaultSettings: Settings = {
|
||||
showReasoningSummaries: false,
|
||||
shellToolPartsExpanded: false,
|
||||
editToolPartsExpanded: false,
|
||||
showSessionProgressBar: true,
|
||||
},
|
||||
updates: {
|
||||
startup: true,
|
||||
@@ -110,6 +125,7 @@ const defaultSettings: Settings = {
|
||||
fontSize: 14,
|
||||
mono: "",
|
||||
sans: "",
|
||||
terminal: "",
|
||||
},
|
||||
keybinds: {},
|
||||
permissions: {
|
||||
@@ -213,6 +229,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
setEditToolPartsExpanded(value: boolean) {
|
||||
setStore("general", "editToolPartsExpanded", value)
|
||||
},
|
||||
showSessionProgressBar: withFallback(
|
||||
() => store.general?.showSessionProgressBar,
|
||||
defaultSettings.general.showSessionProgressBar,
|
||||
),
|
||||
setShowSessionProgressBar(value: boolean) {
|
||||
setStore("general", "showSessionProgressBar", value)
|
||||
},
|
||||
},
|
||||
updates: {
|
||||
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
|
||||
@@ -233,6 +256,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
setUIFont(value: string) {
|
||||
setStore("appearance", "sans", value.trim() ? value : "")
|
||||
},
|
||||
terminalFont: withFallback(() => store.appearance?.terminal, defaultSettings.appearance.terminal),
|
||||
setTerminalFont(value: string) {
|
||||
setStore("appearance", "terminal", value.trim() ? value : "")
|
||||
},
|
||||
},
|
||||
keybinds: {
|
||||
get: (action: string) => store.keybinds?.[action],
|
||||
|
||||
Generated
+5
-1
@@ -564,7 +564,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "السمة",
|
||||
"settings.general.row.theme.description": "تخصيص سمة Kilo.",
|
||||
"settings.general.row.font.title": "خط الكود",
|
||||
"settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية والطرفيات",
|
||||
"settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "خط الواجهة",
|
||||
"settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها",
|
||||
"settings.general.row.followup.title": "سلوك المتابعة",
|
||||
@@ -579,6 +581,8 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني",
|
||||
"settings.general.row.showSessionProgressBar.title": "إظهار شريط تقدم الجلسة",
|
||||
"settings.general.row.showSessionProgressBar.description": "عرض شريط التقدم المتحرك أعلى الجلسة أثناء عمل الوكيل",
|
||||
"settings.general.row.wayland.title": "استخدام Wayland الأصلي",
|
||||
"settings.general.row.wayland.description": "تعطيل التراجع إلى X11 على Wayland. يتطلب إعادة التشغيل.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -572,7 +572,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Personalize como o Kilo é tematizado.",
|
||||
"settings.general.row.font.title": "Fonte de código",
|
||||
"settings.general.row.font.description": "Personalize a fonte usada em blocos de código e terminais",
|
||||
"settings.general.row.font.description": "Personalize a fonte usada em blocos de código",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "Fonte da interface",
|
||||
"settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface",
|
||||
"settings.general.row.followup.title": "Comportamento de acompanhamento",
|
||||
@@ -588,6 +590,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Mostrar partes das ferramentas de edição, escrita e patch expandidas por padrão na linha do tempo",
|
||||
"settings.general.row.showSessionProgressBar.title": "Mostrar barra de progresso da sessão",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Exibir a barra de progresso animada no topo da sessão quando o agente estiver trabalhando",
|
||||
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
||||
"settings.general.row.wayland.description": "Desabilitar fallback X11 no Wayland. Requer reinicialização.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -637,7 +637,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Prilagodi temu Kilo-a.",
|
||||
"settings.general.row.font.title": "Font za kod",
|
||||
"settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda i terminalima",
|
||||
"settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UI font",
|
||||
"settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu",
|
||||
"settings.general.row.followup.title": "Ponašanje nadovezivanja",
|
||||
@@ -653,6 +655,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci",
|
||||
"settings.general.row.showSessionProgressBar.title": "Prikaži traku napretka sesije",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Prikaži animiranu traku napretka na vrhu sesije kada agent radi",
|
||||
"settings.general.row.wayland.title": "Koristi nativni Wayland",
|
||||
"settings.general.row.wayland.description": "Onemogući X11 fallback na Waylandu. Zahtijeva restart.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -632,7 +632,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Tilpas hvordan Kilo er temabestemt.",
|
||||
"settings.general.row.font.title": "Kode-skrifttype",
|
||||
"settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke og terminaler",
|
||||
"settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UI-skrifttype",
|
||||
"settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugerfladen",
|
||||
"settings.general.row.followup.title": "Opfølgningsadfærd",
|
||||
@@ -647,6 +649,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Vis edit-, write- og patch-værktøjsdele udvidet som standard i tidslinjen",
|
||||
"settings.general.row.showSessionProgressBar.title": "Vis sessionens fremdriftslinje",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Vis den animerede fremdriftslinje øverst i sessionen, når agenten arbejder",
|
||||
"settings.general.row.wayland.title": "Brug native Wayland",
|
||||
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Kræver genstart.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -581,7 +581,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Thema",
|
||||
"settings.general.row.theme.description": "Das Thema von Kilo anpassen.",
|
||||
"settings.general.row.font.title": "Code-Schriftart",
|
||||
"settings.general.row.font.description": "Die in Codeblöcken und Terminals verwendete Schriftart anpassen",
|
||||
"settings.general.row.font.description": "Die in Codeblöcken verwendete Schriftart anpassen",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UI-Schriftart",
|
||||
"settings.general.row.uiFont.description": "Die im gesamten Interface verwendete Schriftart anpassen",
|
||||
"settings.general.row.followup.title": "Verhalten bei Folgefragen",
|
||||
@@ -598,6 +600,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Edit-, Write- und Patch-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
|
||||
"settings.general.row.showSessionProgressBar.title": "Sitzungsfortschrittsleiste anzeigen",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Die animierte Fortschrittsleiste oben in der Sitzung anzeigen, wenn der Agent arbeitet",
|
||||
"settings.general.row.wayland.title": "Natives Wayland verwenden",
|
||||
"settings.general.row.wayland.description": "X11-Fallback unter Wayland deaktivieren. Erfordert Neustart.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
@@ -735,7 +735,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Theme",
|
||||
"settings.general.row.theme.description": "Customise how Kilo is themed.",
|
||||
"settings.general.row.font.title": "Code Font",
|
||||
"settings.general.row.font.description": "Customise the font used in code blocks and terminals",
|
||||
"settings.general.row.font.description": "Customise the font used in code blocks",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UI Font",
|
||||
"settings.general.row.uiFont.description": "Customise the font used throughout the interface",
|
||||
"settings.general.row.followup.title": "Follow-up behavior",
|
||||
@@ -760,6 +762,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Expand edit tool parts",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Show edit, write, and patch tool parts expanded by default in the timeline",
|
||||
"settings.general.row.showSessionProgressBar.title": "Show session progress bar",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Display the animated progress bar at the top of the session when the agent is working",
|
||||
|
||||
"settings.general.row.wayland.title": "Use native Wayland",
|
||||
"settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.",
|
||||
|
||||
Generated
+6
-1
@@ -640,7 +640,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Personaliza el tema de Kilo.",
|
||||
"settings.general.row.font.title": "Fuente de código",
|
||||
"settings.general.row.font.description": "Personaliza la fuente usada en bloques de código y terminales",
|
||||
"settings.general.row.font.description": "Personaliza la fuente usada en bloques de código",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "Fuente de la interfaz",
|
||||
"settings.general.row.uiFont.description": "Personaliza la fuente usada en toda la interfaz",
|
||||
"settings.general.row.followup.title": "Comportamiento de seguimiento",
|
||||
@@ -657,6 +659,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Expandir partes de la herramienta de edición",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Mostrar las partes de las herramientas de edición, escritura y parcheado expandidas por defecto en la línea de tiempo",
|
||||
"settings.general.row.showSessionProgressBar.title": "Mostrar barra de progreso de la sesión",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Mostrar la barra de progreso animada en la parte superior de la sesión cuando el agente esté trabajando",
|
||||
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
||||
"settings.general.row.wayland.description": "Deshabilitar fallback a X11 en Wayland. Requiere reinicio.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -579,7 +579,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Thème",
|
||||
"settings.general.row.theme.description": "Personnaliser le thème d'Kilo.",
|
||||
"settings.general.row.font.title": "Police de code",
|
||||
"settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code et les terminaux",
|
||||
"settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "Police de l'interface",
|
||||
"settings.general.row.uiFont.description": "Personnaliser la police utilisée dans toute l'interface",
|
||||
"settings.general.row.followup.title": "Comportement de suivi",
|
||||
@@ -596,6 +598,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Développer les parties de l'outil edit",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Afficher les parties des outils edit, write et patch développées par défaut dans la chronologie",
|
||||
"settings.general.row.showSessionProgressBar.title": "Afficher la barre de progression de la session",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Afficher la barre de progression animée en haut de la session lorsque l'agent travaille",
|
||||
"settings.general.row.wayland.title": "Utiliser Wayland natif",
|
||||
"settings.general.row.wayland.description": "Désactiver le repli X11 sur Wayland. Nécessite un redémarrage.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -569,7 +569,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "テーマ",
|
||||
"settings.general.row.theme.description": "Kiloのテーマをカスタマイズします。",
|
||||
"settings.general.row.font.title": "コードフォント",
|
||||
"settings.general.row.font.description": "コードブロックとターミナルで使用するフォントをカスタマイズします",
|
||||
"settings.general.row.font.description": "コードブロックで使用するフォントをカスタマイズします",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UIフォント",
|
||||
"settings.general.row.uiFont.description": "インターフェース全体で使用するフォントをカスタマイズします",
|
||||
"settings.general.row.followup.title": "フォローアップの動作",
|
||||
@@ -585,6 +587,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "edit ツールパーツを展開",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"タイムラインで edit、write、patch ツールパーツをデフォルトで展開して表示します",
|
||||
"settings.general.row.showSessionProgressBar.title": "セッション進行状況バーを表示",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"エージェントの作業中に、セッション上部にアニメーション付きの進行状況バーを表示します",
|
||||
"settings.general.row.wayland.title": "ネイティブWaylandを使用",
|
||||
"settings.general.row.wayland.description": "WaylandでのX11フォールバックを無効にします。再起動が必要です。",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -566,7 +566,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "테마",
|
||||
"settings.general.row.theme.description": "Kilo 테마 사용자 지정",
|
||||
"settings.general.row.font.title": "코드 글꼴",
|
||||
"settings.general.row.font.description": "코드 블록과 터미널에 사용되는 글꼴을 사용자 지정",
|
||||
"settings.general.row.font.description": "코드 블록에 사용되는 글꼴을 사용자 지정",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UI 글꼴",
|
||||
"settings.general.row.uiFont.description": "인터페이스 전반에 사용되는 글꼴을 사용자 지정",
|
||||
"settings.general.row.followup.title": "후속 조치 동작",
|
||||
@@ -581,6 +583,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "edit 도구 파트 펼치기",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"타임라인에서 기본적으로 edit, write, patch 도구 파트를 펼친 상태로 표시합니다",
|
||||
"settings.general.row.showSessionProgressBar.title": "세션 진행 표시줄 표시",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"에이전트가 작업 중일 때 세션 상단에 애니메이션 진행 표시줄을 표시합니다",
|
||||
"settings.general.row.wayland.title": "네이티브 Wayland 사용",
|
||||
"settings.general.row.wayland.description": "Wayland에서 X11 폴백을 비활성화합니다. 다시 시작해야 합니다.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -640,7 +640,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Tilpass hvordan Kilo er tematisert.",
|
||||
"settings.general.row.font.title": "Kodefont",
|
||||
"settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker og terminaler",
|
||||
"settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UI-skrift",
|
||||
"settings.general.row.uiFont.description": "Tilpass skrifttypen som brukes i hele grensesnittet",
|
||||
"settings.general.row.followup.title": "Oppfølgingsadferd",
|
||||
@@ -654,6 +656,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Utvid edit-verktøydeler",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Vis edit-, write- og patch-verktøydeler utvidet som standard i tidslinjen",
|
||||
"settings.general.row.showSessionProgressBar.title": "Vis fremdriftslinje for sesjonen",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Vis den animerte fremdriftslinjen øverst i sesjonen når agenten jobber",
|
||||
"settings.general.row.wayland.title": "Bruk innebygd Wayland",
|
||||
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Krever omstart.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -571,7 +571,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Motyw",
|
||||
"settings.general.row.theme.description": "Dostosuj motyw Kilo.",
|
||||
"settings.general.row.font.title": "Czcionka kodu",
|
||||
"settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu i terminalach",
|
||||
"settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "Czcionka interfejsu",
|
||||
"settings.general.row.uiFont.description": "Dostosuj czcionkę używaną w całym interfejsie",
|
||||
"settings.general.row.followup.title": "Zachowanie kontynuacji",
|
||||
@@ -586,6 +588,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Rozwijaj elementy narzędzia edit",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Domyślnie pokazuj rozwinięte elementy narzędzi edit, write i patch na osi czasu",
|
||||
"settings.general.row.showSessionProgressBar.title": "Pokazuj pasek postępu sesji",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Wyświetlaj animowany pasek postępu u góry sesji, gdy agent pracuje",
|
||||
"settings.general.row.wayland.title": "Użyj natywnego Wayland",
|
||||
"settings.general.row.wayland.description": "Wyłącz fallback X11 na Wayland. Wymaga restartu.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -637,7 +637,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Тема",
|
||||
"settings.general.row.theme.description": "Настройте оформление Kilo.",
|
||||
"settings.general.row.font.title": "Шрифт кода",
|
||||
"settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода и терминалах",
|
||||
"settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "Шрифт интерфейса",
|
||||
"settings.general.row.uiFont.description": "Настройте шрифт, используемый во всем интерфейсе",
|
||||
"settings.general.row.followup.title": "Поведение уточняющих вопросов",
|
||||
@@ -654,6 +656,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Разворачивать элементы инструмента edit",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Показывать элементы инструментов edit, write и patch в ленте развернутыми по умолчанию",
|
||||
"settings.general.row.showSessionProgressBar.title": "Показывать индикатор прогресса сессии",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Показывать анимированный индикатор прогресса вверху сессии, когда агент работает",
|
||||
"settings.general.row.wayland.title": "Использовать нативный Wayland",
|
||||
"settings.general.row.wayland.description": "Отключить X11 fallback на Wayland. Требуется перезапуск.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -631,7 +631,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "ธีม",
|
||||
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ Kilo มีธีม",
|
||||
"settings.general.row.font.title": "ฟอนต์โค้ด",
|
||||
"settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ดและเทอร์มินัล",
|
||||
"settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ด",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "ฟอนต์ UI",
|
||||
"settings.general.row.uiFont.description": "ปรับแต่งฟอนต์ที่ใช้ทั่วทั้งอินเทอร์เฟซ",
|
||||
"settings.general.row.followup.title": "พฤติกรรมการติดตามผล",
|
||||
@@ -645,6 +647,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"แสดงส่วนเครื่องมือ edit, write และ patch แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
|
||||
"settings.general.row.showSessionProgressBar.title": "แสดงแถบความคืบหน้าของเซสชัน",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"แสดงแถบความคืบหน้าแบบเคลื่อนไหวที่ด้านบนของเซสชันเมื่อเอเจนต์กำลังทำงาน",
|
||||
"settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ",
|
||||
"settings.general.row.wayland.description": "ปิดใช้งาน X11 fallback บน Wayland ต้องรีสตาร์ท",
|
||||
"settings.general.row.wayland.tooltip": "บน Linux ที่มีจอภาพรีเฟรชเรตแบบผสม Wayland แบบเนทีฟอาจเสถียรกว่า",
|
||||
|
||||
Generated
+7
-1
@@ -642,7 +642,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Kilo'un temasını özelleştirin.",
|
||||
"settings.general.row.font.title": "Kod Yazı Tipi",
|
||||
"settings.general.row.font.description": "Kod bloklarında ve terminallerde kullanılan yazı tipini özelleştirin",
|
||||
"settings.general.row.font.description": "Kod bloklarında kullanılan yazı tipini özelleştirin",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "Arayüz Yazı Tipi",
|
||||
"settings.general.row.uiFont.description": "Arayüz genelinde kullanılan yazı tipini özelleştirin",
|
||||
"settings.general.row.followup.title": "Takip davranışı",
|
||||
@@ -659,6 +661,10 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Zaman çizelgesinde düzenleme, yazma ve yama araç bileşenlerini varsayılan olarak genişletilmiş göster",
|
||||
|
||||
"settings.general.row.showSessionProgressBar.title": "Oturum ilerleme çubuğunu göster",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Ajan çalışırken oturumun üst kısmında animasyonlu ilerleme çubuğunu göster",
|
||||
|
||||
"settings.general.row.wayland.title": "Yerel Wayland kullan",
|
||||
"settings.general.row.wayland.description":
|
||||
"Wayland'da X11 geri dönüşünü devre dışı bırak. Yeniden başlatma gerektirir.",
|
||||
|
||||
Generated
+5
-1
@@ -631,7 +631,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "主题",
|
||||
"settings.general.row.theme.description": "自定义 Kilo 的主题。",
|
||||
"settings.general.row.font.title": "代码字体",
|
||||
"settings.general.row.font.description": "自定义代码块和终端使用的字体",
|
||||
"settings.general.row.font.description": "自定义代码块使用的字体",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "界面字体",
|
||||
"settings.general.row.uiFont.description": "自定义整个界面使用的字体",
|
||||
"settings.general.row.followup.title": "跟进消息行为",
|
||||
@@ -644,6 +646,8 @@ export const dict = {
|
||||
"settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
|
||||
"settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
|
||||
"settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具部分",
|
||||
"settings.general.row.showSessionProgressBar.title": "显示会话进度条",
|
||||
"settings.general.row.showSessionProgressBar.description": "当智能体正在工作时,在会话顶部显示动画进度条",
|
||||
"settings.general.row.wayland.title": "使用原生 Wayland",
|
||||
"settings.general.row.wayland.description": "在 Wayland 上禁用 X11 回退。需要重启。",
|
||||
"settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。",
|
||||
|
||||
Generated
+5
-1
@@ -626,7 +626,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "主題",
|
||||
"settings.general.row.theme.description": "自訂 Kilo 的主題。",
|
||||
"settings.general.row.font.title": "程式碼字型",
|
||||
"settings.general.row.font.description": "自訂程式碼區塊和終端機使用的字型",
|
||||
"settings.general.row.font.description": "自訂程式碼區塊使用的字型",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "介面字型",
|
||||
"settings.general.row.uiFont.description": "自訂整個介面使用的字型",
|
||||
"settings.general.row.followup.title": "後續追問行為",
|
||||
@@ -640,6 +642,8 @@ export const dict = {
|
||||
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
|
||||
"settings.general.row.editToolPartsExpanded.title": "展開 edit 工具區塊",
|
||||
"settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊",
|
||||
"settings.general.row.showSessionProgressBar.title": "顯示工作階段進度列",
|
||||
"settings.general.row.showSessionProgressBar.description": "當代理程式正在運作時,在工作階段頂部顯示動畫進度列",
|
||||
"settings.general.row.wayland.title": "使用原生 Wayland",
|
||||
"settings.general.row.wayland.description": "在 Wayland 上停用 X11 後備模式。需要重新啟動。",
|
||||
"settings.general.row.wayland.tooltip": "在混合更新率螢幕的 Linux 系統上,原生 Wayland 可能更穩定。",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
@import "@opencode-ai/ui/styles/tailwind";
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrainsMono Nerd Font Mono";
|
||||
src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
@keyframes session-progress-whip {
|
||||
0% {
|
||||
@@ -66,4 +73,13 @@
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DataProvider } from "@opencode-ai/ui/context"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||
import { createEffect, createMemo, type ParentProps, Show } from "solid-js"
|
||||
import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { LocalProvider } from "@/context/local"
|
||||
import { SDKProvider } from "@/context/sdk"
|
||||
@@ -23,11 +23,10 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
||||
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
void sync.session.sync(id)
|
||||
})
|
||||
createResource(
|
||||
() => params.id,
|
||||
(id) => sync.session.sync(id),
|
||||
)
|
||||
|
||||
return (
|
||||
<DataProvider
|
||||
|
||||
@@ -31,7 +31,7 @@ function sortSessions(now: number) {
|
||||
const isRootVisibleSession = (session: Session, directory: string) =>
|
||||
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
|
||||
|
||||
const roots = (store: SessionStore) =>
|
||||
export const roots = (store: SessionStore) =>
|
||||
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
|
||||
|
||||
export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now))
|
||||
|
||||
@@ -19,6 +19,12 @@ import { childSessionOnPath, hasProjectPermissions } from "./helpers"
|
||||
|
||||
const KILO_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
|
||||
export function getProjectAvatarSource(id?: string, icon?: { color?: string; url?: string; override?: string }) {
|
||||
return id === KILO_PROJECT_ID
|
||||
? "https://kilo.ai/favicon.svg"
|
||||
: (icon?.override ?? (icon?.color ? undefined : icon?.url))
|
||||
}
|
||||
|
||||
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const notification = useNotification()
|
||||
@@ -42,9 +48,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
|
||||
<div class="size-full rounded overflow-clip">
|
||||
<Avatar
|
||||
fallback={name()}
|
||||
src={
|
||||
props.project.id === KILO_PROJECT_ID ? "https://kilo.ai/favicon.svg" : props.project.icon?.override
|
||||
}
|
||||
src={getProjectAvatarSource(props.project.id, props.project.icon)}
|
||||
{...getAvatarColors(props.project.icon?.color)}
|
||||
class="size-full rounded"
|
||||
classList={{ "badge-mask": notify() }}
|
||||
@@ -263,10 +267,10 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={currentChild()}>
|
||||
<Show when={currentChild()} keyed>
|
||||
{(child) => (
|
||||
<div class="w-full">
|
||||
<SessionItem {...props} session={child()} level={(props.level ?? 0) + 1} />
|
||||
<SessionItem {...props} session={child} level={(props.level ?? 0) + 1} />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
@@ -321,7 +321,7 @@ export const SortableWorkspace = (props: {
|
||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
|
||||
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
|
||||
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
||||
const loading = () => query.isLoading
|
||||
const loading = () => query.isLoading && count() === 0
|
||||
const touch = createMediaQuery("(hover: none)")
|
||||
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
|
||||
const loadMore = async () => {
|
||||
|
||||
@@ -259,7 +259,7 @@ export function MessageTimeline(props: {
|
||||
if (!id) return idle
|
||||
return sync.data.session_status[id] ?? idle
|
||||
})
|
||||
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
|
||||
const working = createMemo(() => sessionStatus().type !== "idle")
|
||||
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
|
||||
|
||||
const [timeoutDone, setTimeoutDone] = createSignal(true)
|
||||
@@ -721,7 +721,7 @@ export function MessageTimeline(props: {
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<Show when={workingStatus() !== "hidden"}>
|
||||
<Show when={workingStatus() !== "hidden" && settings.general.showSessionProgressBar()}>
|
||||
<div
|
||||
data-component="session-progress"
|
||||
data-state={workingStatus()}
|
||||
@@ -812,7 +812,7 @@ export function MessageTimeline(props: {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={sessionID()}>
|
||||
<Show when={sessionID()} keyed>
|
||||
{(id) => (
|
||||
<div class="shrink-0 flex items-center gap-3">
|
||||
<SessionContextUsage placement="bottom" />
|
||||
@@ -878,12 +878,12 @@ export function MessageTimeline(props: {
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
||||
<DropdownMenu.Item onSelect={() => void archiveSession(id)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
|
||||
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
@@ -6,7 +6,7 @@ FROM ${REGISTRY}/build/base:24.04
|
||||
SHELL ["/bin/bash", "-lc"]
|
||||
|
||||
ARG NODE_VERSION=24.4.0
|
||||
ARG BUN_VERSION=1.3.11
|
||||
ARG BUN_VERSION=1.3.13
|
||||
|
||||
ENV BUN_INSTALL=/opt/bun
|
||||
ENV PATH=/opt/bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
@@ -53,6 +53,10 @@ export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: { index: "src/preload/index.ts" },
|
||||
output: {
|
||||
format: "cjs",
|
||||
entryFileNames: "[name].js",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
@@ -30,6 +30,7 @@
|
||||
"electron-store": "^10",
|
||||
"electron-updater": "^6",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"drizzle-orm": "catalog:",
|
||||
"marked": "^15",
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -53,7 +54,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@valibot/to-json-schema": "1.6.0",
|
||||
"electron": "40.8.5",
|
||||
"electron": "41.2.1",
|
||||
"electron-builder": "^26",
|
||||
"electron-vite": "^5",
|
||||
"solid-js": "catalog:",
|
||||
|
||||
@@ -28,8 +28,10 @@ const APP_IDS: Record<string, string> = {
|
||||
beta: "ai.opencode.desktop.beta",
|
||||
prod: "ai.opencode.desktop",
|
||||
}
|
||||
const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"
|
||||
app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev")
|
||||
app.setPath("userData", join(app.getPath("appData"), app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"))
|
||||
app.setAppUserModelId(appId)
|
||||
app.setPath("userData", join(app.getPath("appData"), appId))
|
||||
const { autoUpdater } = pkg
|
||||
|
||||
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
|
||||
@@ -40,7 +42,14 @@ import { initLogging } from "./logging"
|
||||
import { parseMarkdown } from "./markdown"
|
||||
import { createMenu } from "./menu"
|
||||
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
|
||||
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
|
||||
import {
|
||||
createLoadingWindow,
|
||||
createMainWindow,
|
||||
registerRendererProtocol,
|
||||
setBackgroundColor,
|
||||
setDockIcon,
|
||||
} from "./windows"
|
||||
import { drizzle } from "drizzle-orm/node-sqlite/driver"
|
||||
import type { Server } from "virtual:opencode-server"
|
||||
|
||||
const initEmitter = new EventEmitter()
|
||||
@@ -103,6 +112,7 @@ function setupApp() {
|
||||
|
||||
void app.whenReady().then(async () => {
|
||||
app.setAsDefaultProtocolClient("opencode")
|
||||
registerRendererProtocol()
|
||||
setDockIcon()
|
||||
setupAutoUpdater()
|
||||
await initialize()
|
||||
@@ -137,15 +147,6 @@ async function initialize() {
|
||||
const url = `http://${hostname}:${port}`
|
||||
const password = randomUUID()
|
||||
|
||||
logger.log("spawning sidecar", { url })
|
||||
const { listener, health } = await spawnLocalServer(hostname, port, password)
|
||||
server = listener
|
||||
serverReady.resolve({
|
||||
url,
|
||||
username: "kilo", // kilocode_change
|
||||
password,
|
||||
})
|
||||
|
||||
const loadingTask = (async () => {
|
||||
logger.log("sidecar connection started", { url })
|
||||
|
||||
@@ -156,10 +157,32 @@ async function initialize() {
|
||||
if (progress.type === "Done") sqliteDone?.resolve()
|
||||
})
|
||||
|
||||
if (needsMigration) {
|
||||
const { Database, JsonMigration } = await import("virtual:opencode-server")
|
||||
await JsonMigration.run(drizzle({ client: Database.Client().$client }), {
|
||||
progress: (event: { current: number; total: number }) => {
|
||||
const percent = Math.round(event.current / event.total) * 100
|
||||
initEmitter.emit("sqlite", { type: "InProgress", value: percent })
|
||||
},
|
||||
})
|
||||
initEmitter.emit("sqlite", { type: "Done" })
|
||||
|
||||
sqliteDone?.resolve()
|
||||
}
|
||||
|
||||
if (needsMigration) {
|
||||
await sqliteDone?.promise
|
||||
}
|
||||
|
||||
logger.log("spawning sidecar", { url })
|
||||
const { listener, health } = await spawnLocalServer(hostname, port, password)
|
||||
server = listener
|
||||
serverReady.resolve({
|
||||
url,
|
||||
username: "kilo", // kilocode_change
|
||||
password,
|
||||
})
|
||||
|
||||
await Promise.race([
|
||||
health.wait,
|
||||
delay(30_000).then(() => {
|
||||
@@ -172,15 +195,10 @@ async function initialize() {
|
||||
logger.log("loading task finished")
|
||||
})()
|
||||
|
||||
const globals = {
|
||||
updaterEnabled: UPDATER_ENABLED,
|
||||
deepLinks: pendingDeepLinks,
|
||||
}
|
||||
|
||||
if (needsMigration) {
|
||||
const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)])
|
||||
if (show) {
|
||||
overlay = createLoadingWindow(globals)
|
||||
overlay = createLoadingWindow()
|
||||
await delay(1_000)
|
||||
}
|
||||
}
|
||||
@@ -192,7 +210,7 @@ async function initialize() {
|
||||
await loadingComplete.promise
|
||||
}
|
||||
|
||||
mainWindow = createMainWindow(globals)
|
||||
mainWindow = createMainWindow()
|
||||
wireMenu()
|
||||
|
||||
overlay?.close()
|
||||
@@ -229,6 +247,8 @@ registerIpcHandlers({
|
||||
initEmitter.off("step", listener)
|
||||
}
|
||||
},
|
||||
getWindowConfig: () => ({ updaterEnabled: UPDATER_ENABLED }),
|
||||
consumeInitialDeepLinks: () => pendingDeepLinks.splice(0),
|
||||
getDefaultServerUrl: () => getDefaultServerUrl(),
|
||||
setDefaultServerUrl: (url) => setDefaultServerUrl(url),
|
||||
getWslConfig: () => Promise.resolve(getWslConfig()),
|
||||
|
||||
@@ -2,7 +2,14 @@ import { execFile } from "node:child_process"
|
||||
import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron"
|
||||
import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
|
||||
|
||||
import type { InitStep, ServerReadyData, SqliteMigrationProgress, TitlebarTheme, WslConfig } from "../preload/types"
|
||||
import type {
|
||||
InitStep,
|
||||
ServerReadyData,
|
||||
SqliteMigrationProgress,
|
||||
TitlebarTheme,
|
||||
WindowConfig,
|
||||
WslConfig,
|
||||
} from "../preload/types"
|
||||
import { getStore } from "./store"
|
||||
import { setTitlebar } from "./windows"
|
||||
|
||||
@@ -14,6 +21,8 @@ const pickerFilters = (ext?: string[]) => {
|
||||
type Deps = {
|
||||
killSidecar: () => void
|
||||
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
||||
getWindowConfig: () => Promise<WindowConfig> | WindowConfig
|
||||
consumeInitialDeepLinks: () => Promise<string[]> | string[]
|
||||
getDefaultServerUrl: () => Promise<string | null> | string | null
|
||||
setDefaultServerUrl: (url: string | null) => Promise<void> | void
|
||||
getWslConfig: () => Promise<WslConfig>
|
||||
@@ -37,6 +46,8 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
const send = (step: InitStep) => event.sender.send("init-step", step)
|
||||
return deps.awaitInitialization(send)
|
||||
})
|
||||
ipcMain.handle("get-window-config", () => deps.getWindowConfig())
|
||||
ipcMain.handle("consume-initial-deep-links", () => deps.consumeInitialDeepLinks())
|
||||
ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl())
|
||||
ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
|
||||
deps.setDefaultServerUrl(url),
|
||||
|
||||
@@ -47,7 +47,7 @@ export function createMenu(deps: Deps) {
|
||||
{
|
||||
label: "New Window",
|
||||
accelerator: "Cmd+Shift+N",
|
||||
click: () => createMainWindow({ updaterEnabled: UPDATER_ENABLED }),
|
||||
click: () => createMainWindow(),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "close" },
|
||||
|
||||
@@ -4,7 +4,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { CHANNEL } from "./constants"
|
||||
import { getStore, store } from "./store"
|
||||
import { getStore } from "./store"
|
||||
|
||||
const TAURI_MIGRATED_KEY = "tauriMigrated"
|
||||
|
||||
@@ -67,7 +67,7 @@ function migrateFile(datPath: string, filename: string) {
|
||||
}
|
||||
|
||||
export function migrate() {
|
||||
if (store.get(TAURI_MIGRATED_KEY)) {
|
||||
if (getStore().get(TAURI_MIGRATED_KEY)) {
|
||||
log.log("tauri migration: already done, skipping")
|
||||
return
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export function migrate() {
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
log.log("tauri migration: no tauri data directory found, nothing to migrate")
|
||||
store.set(TAURI_MIGRATED_KEY, true)
|
||||
getStore().set(TAURI_MIGRATED_KEY, true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,5 +87,5 @@ export function migrate() {
|
||||
}
|
||||
|
||||
log.log("tauri migration: complete")
|
||||
store.set(TAURI_MIGRATED_KEY, true)
|
||||
getStore().set(TAURI_MIGRATED_KEY, true)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { app } from "electron"
|
||||
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
|
||||
import { getUserShell, loadShellEnv } from "./shell-env"
|
||||
import { store } from "./store"
|
||||
import { getStore } from "./store"
|
||||
|
||||
export type WslConfig = { enabled: boolean }
|
||||
|
||||
export type HealthCheck = { wait: Promise<void> }
|
||||
|
||||
export function getDefaultServerUrl(): string | null {
|
||||
const value = store.get(DEFAULT_SERVER_URL_KEY)
|
||||
const value = getStore().get(DEFAULT_SERVER_URL_KEY)
|
||||
return typeof value === "string" ? value : null
|
||||
}
|
||||
|
||||
export function setDefaultServerUrl(url: string | null) {
|
||||
if (url) {
|
||||
store.set(DEFAULT_SERVER_URL_KEY, url)
|
||||
getStore().set(DEFAULT_SERVER_URL_KEY, url)
|
||||
return
|
||||
}
|
||||
|
||||
store.delete(DEFAULT_SERVER_URL_KEY)
|
||||
getStore().delete(DEFAULT_SERVER_URL_KEY)
|
||||
}
|
||||
|
||||
export function getWslConfig(): WslConfig {
|
||||
const value = store.get(WSL_ENABLED_KEY)
|
||||
const value = getStore().get(WSL_ENABLED_KEY)
|
||||
return { enabled: typeof value === "boolean" ? value : false }
|
||||
}
|
||||
|
||||
export function setWslConfig(config: WslConfig) {
|
||||
store.set(WSL_ENABLED_KEY, config.enabled)
|
||||
getStore().set(WSL_ENABLED_KEY, config.enabled)
|
||||
}
|
||||
|
||||
export async function spawnLocalServer(hostname: string, port: number, password: string) {
|
||||
@@ -39,6 +39,7 @@ export async function spawnLocalServer(hostname: string, port: number, password:
|
||||
hostname,
|
||||
username: "opencode",
|
||||
password,
|
||||
cors: ["oc://renderer"],
|
||||
})
|
||||
|
||||
const wait = (async () => {
|
||||
|
||||
@@ -4,6 +4,10 @@ import { SETTINGS_STORE } from "./constants"
|
||||
|
||||
const cache = new Map<string, Store>()
|
||||
|
||||
// We cannot instantiate the electron-store at module load time because
|
||||
// module import hoisting causes this to run before app.setPath("userData", ...)
|
||||
// in index.ts has executed, which would result in files being written to the default directory
|
||||
// (e.g. bad: %APPDATA%\@opencode-ai\desktop-electron\opencode.settings vs good: %APPDATA%\ai.opencode.desktop.dev\opencode.settings).
|
||||
export function getStore(name = SETTINGS_STORE) {
|
||||
const cached = cache.get(name)
|
||||
if (cached) return cached
|
||||
@@ -11,5 +15,3 @@ export function getStore(name = SETTINGS_STORE) {
|
||||
cache.set(name, next)
|
||||
return next
|
||||
}
|
||||
|
||||
export const store = getStore(SETTINGS_STORE)
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import windowState from "electron-window-state"
|
||||
import { app, BrowserWindow, nativeImage, nativeTheme } from "electron"
|
||||
import { dirname, join } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { app, BrowserWindow, net, nativeImage, nativeTheme, protocol } from "electron"
|
||||
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
||||
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||
import type { TitlebarTheme } from "../preload/types"
|
||||
|
||||
type Globals = {
|
||||
updaterEnabled: boolean
|
||||
deepLinks?: string[]
|
||||
}
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url))
|
||||
const rendererRoot = join(root, "../renderer")
|
||||
const rendererProtocol = "oc"
|
||||
const rendererHost = "renderer"
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: rendererProtocol,
|
||||
privileges: {
|
||||
secure: true,
|
||||
standard: true,
|
||||
supportFetchAPI: true,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
let backgroundColor: string | undefined
|
||||
|
||||
@@ -54,7 +63,7 @@ export function setDockIcon() {
|
||||
if (!icon.isEmpty()) app.dock?.setIcon(icon)
|
||||
}
|
||||
|
||||
export function createMainWindow(globals: Globals) {
|
||||
export function createMainWindow() {
|
||||
const state = windowState({
|
||||
defaultWidth: 1280,
|
||||
defaultHeight: 800,
|
||||
@@ -84,15 +93,29 @@ export function createMainWindow(globals: Globals) {
|
||||
}
|
||||
: {}),
|
||||
webPreferences: {
|
||||
preload: join(root, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
preload: join(root, "../preload/index.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
},
|
||||
})
|
||||
|
||||
win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||
const { requestHeaders } = details
|
||||
upsertKeyValue(requestHeaders, "Access-Control-Allow-Origin", ["*"])
|
||||
callback({ requestHeaders })
|
||||
})
|
||||
|
||||
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
const { responseHeaders = {} } = details
|
||||
upsertKeyValue(responseHeaders, "Access-Control-Allow-Origin", ["*"])
|
||||
upsertKeyValue(responseHeaders, "Access-Control-Allow-Headers", ["*"])
|
||||
callback({ responseHeaders })
|
||||
})
|
||||
|
||||
state.manage(win)
|
||||
loadWindow(win, "index.html")
|
||||
wireZoom(win)
|
||||
injectGlobals(win, globals)
|
||||
|
||||
win.once("ready-to-show", () => {
|
||||
win.show()
|
||||
@@ -101,7 +124,7 @@ export function createMainWindow(globals: Globals) {
|
||||
return win
|
||||
}
|
||||
|
||||
export function createLoadingWindow(globals: Globals) {
|
||||
export function createLoadingWindow() {
|
||||
const mode = tone()
|
||||
const win = new BrowserWindow({
|
||||
width: 640,
|
||||
@@ -120,17 +143,37 @@ export function createLoadingWindow(globals: Globals) {
|
||||
}
|
||||
: {}),
|
||||
webPreferences: {
|
||||
preload: join(root, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
preload: join(root, "../preload/index.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
},
|
||||
})
|
||||
|
||||
loadWindow(win, "loading.html")
|
||||
injectGlobals(win, globals)
|
||||
|
||||
return win
|
||||
}
|
||||
|
||||
export function registerRendererProtocol() {
|
||||
if (protocol.isProtocolHandled(rendererProtocol)) return
|
||||
|
||||
protocol.handle(rendererProtocol, (request) => {
|
||||
const url = new URL(request.url)
|
||||
if (url.host !== rendererHost) {
|
||||
return new Response("Not found", { status: 404 })
|
||||
}
|
||||
|
||||
const file = resolve(rendererRoot, `.${decodeURIComponent(url.pathname)}`)
|
||||
const rel = relative(rendererRoot, file)
|
||||
if (rel.startsWith("..") || isAbsolute(rel)) {
|
||||
return new Response("Not found", { status: 404 })
|
||||
}
|
||||
|
||||
return net.fetch(pathToFileURL(file).toString())
|
||||
})
|
||||
}
|
||||
|
||||
function loadWindow(win: BrowserWindow, html: string) {
|
||||
const devUrl = process.env.ELECTRON_RENDERER_URL
|
||||
if (devUrl) {
|
||||
@@ -139,25 +182,25 @@ function loadWindow(win: BrowserWindow, html: string) {
|
||||
return
|
||||
}
|
||||
|
||||
void win.loadFile(join(root, `../renderer/${html}`))
|
||||
void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`)
|
||||
}
|
||||
|
||||
function injectGlobals(win: BrowserWindow, globals: Globals) {
|
||||
win.webContents.on("dom-ready", () => {
|
||||
const deepLinks = globals.deepLinks ?? []
|
||||
const data = {
|
||||
updaterEnabled: globals.updaterEnabled,
|
||||
deepLinks: Array.isArray(deepLinks) ? deepLinks.splice(0) : deepLinks,
|
||||
}
|
||||
void win.webContents.executeJavaScript(
|
||||
`window.__KILO__ = Object.assign(window.__KILO__ ?? {}, ${JSON.stringify(data)})`,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function wireZoom(win: BrowserWindow) {
|
||||
win.webContents.setZoomFactor(1)
|
||||
win.webContents.on("zoom-changed", () => {
|
||||
win.webContents.setZoomFactor(1)
|
||||
})
|
||||
}
|
||||
|
||||
function upsertKeyValue(obj: Record<string, any>, keyToChange: string, value: any) {
|
||||
const keyToChangeLower = keyToChange.toLowerCase()
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (key.toLowerCase() === keyToChangeLower) {
|
||||
// Reassign old key
|
||||
obj[key] = value
|
||||
// Done
|
||||
return
|
||||
}
|
||||
}
|
||||
// Insert at end instead
|
||||
obj[keyToChange] = value
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ const api: ElectronAPI = {
|
||||
ipcRenderer.removeListener("init-step", handler)
|
||||
})
|
||||
},
|
||||
getWindowConfig: () => ipcRenderer.invoke("get-window-config"),
|
||||
consumeInitialDeepLinks: () => ipcRenderer.invoke("consume-initial-deep-links"),
|
||||
getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"),
|
||||
setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url),
|
||||
getWslConfig: () => ipcRenderer.invoke("get-wsl-config"),
|
||||
|
||||
@@ -15,10 +15,16 @@ export type TitlebarTheme = {
|
||||
mode: "light" | "dark"
|
||||
}
|
||||
|
||||
export type WindowConfig = {
|
||||
updaterEnabled: boolean
|
||||
}
|
||||
|
||||
export type ElectronAPI = {
|
||||
killSidecar: () => Promise<void>
|
||||
installCli: () => Promise<string>
|
||||
awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
||||
getWindowConfig: () => Promise<WindowConfig>
|
||||
consumeInitialDeepLinks: () => Promise<string[]>
|
||||
getDefaultServerUrl: () => Promise<string | null>
|
||||
setDefaultServerUrl: (url: string | null) => Promise<void>
|
||||
getWslConfig: () => Promise<WslConfig>
|
||||
|
||||
@@ -4,8 +4,6 @@ declare global {
|
||||
interface Window {
|
||||
api: ElectronAPI
|
||||
__OPENCODE__?: {
|
||||
updaterEnabled?: boolean
|
||||
wsl?: boolean
|
||||
deepLinks?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ const root = resolve(dir, "../..")
|
||||
const html = async (name: string) => Bun.file(join(dir, name)).text()
|
||||
|
||||
/**
|
||||
* Electron loads renderer HTML via `win.loadFile()` which uses the `file://`
|
||||
* protocol. Absolute paths like `src="/foo.js"` resolve to the filesystem root
|
||||
* (e.g. `file:///C:/foo.js` on Windows) instead of relative to the app bundle.
|
||||
* Packaged Electron windows load renderer HTML via the privileged `oc://`
|
||||
* protocol. Root-relative asset paths like `src="/foo.js"` would resolve from
|
||||
* the protocol origin root instead of relative to the current HTML entrypoint.
|
||||
*
|
||||
* All local resource references must use relative paths (`./`).
|
||||
*/
|
||||
|
||||
@@ -20,7 +20,6 @@ import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js
|
||||
import { render } from "solid-js/web"
|
||||
import pkg from "../../package.json"
|
||||
import { initI18n, t } from "./i18n"
|
||||
import { UPDATER_ENABLED } from "./updater"
|
||||
import { webviewZoom } from "./webview-zoom"
|
||||
import "./styles.css"
|
||||
import { useTheme } from "@opencode-ai/ui/theme"
|
||||
@@ -43,8 +42,7 @@ const emitDeepLinks = (urls: string[]) => {
|
||||
}
|
||||
|
||||
const listenForDeepLinks = () => {
|
||||
const startUrls = window.__KILO__?.deepLinks ?? []
|
||||
if (startUrls.length) emitDeepLinks(startUrls)
|
||||
void window.api.consumeInitialDeepLinks().then((urls) => emitDeepLinks(urls))
|
||||
return window.api.onDeepLink((urls) => emitDeepLinks(urls))
|
||||
}
|
||||
|
||||
@@ -57,13 +55,21 @@ const createPlatform = (): Platform => {
|
||||
return undefined
|
||||
})()
|
||||
|
||||
const isWslEnabled = async () => {
|
||||
if (os !== "windows") return false
|
||||
return window.api
|
||||
.getWslConfig()
|
||||
.then((config) => config.enabled)
|
||||
.catch(() => false)
|
||||
}
|
||||
|
||||
const wslHome = async () => {
|
||||
if (os !== "windows" || !window.__KILO__?.wsl) return undefined
|
||||
if (!(await isWslEnabled())) return undefined
|
||||
return window.api.wslPath("~", "windows").catch(() => undefined)
|
||||
}
|
||||
|
||||
const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => {
|
||||
if (!result || !window.__KILO__?.wsl) return result
|
||||
if (!result || !(await isWslEnabled())) return result
|
||||
if (Array.isArray(result)) {
|
||||
return Promise.all(result.map((path) => window.api.wslPath(path, "linux").catch(() => path))) as any
|
||||
}
|
||||
@@ -137,7 +143,7 @@ const createPlatform = (): Platform => {
|
||||
if (os === "windows") {
|
||||
const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null
|
||||
const resolvedPath = await (async () => {
|
||||
if (window.__KILO__?.wsl) {
|
||||
if (await isWslEnabled()) {
|
||||
const converted = await window.api.wslPath(path, "windows").catch(() => null)
|
||||
if (converted) return converted
|
||||
}
|
||||
@@ -159,12 +165,14 @@ const createPlatform = (): Platform => {
|
||||
storage,
|
||||
|
||||
checkUpdate: async () => {
|
||||
if (!UPDATER_ENABLED()) return { updateAvailable: false }
|
||||
const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))
|
||||
if (!config.updaterEnabled) return { updateAvailable: false }
|
||||
return window.api.checkUpdate()
|
||||
},
|
||||
|
||||
update: async () => {
|
||||
if (!UPDATER_ENABLED()) return
|
||||
const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))
|
||||
if (!config.updaterEnabled) return
|
||||
await window.api.installUpdate()
|
||||
},
|
||||
|
||||
@@ -194,11 +202,7 @@ const createPlatform = (): Platform => {
|
||||
return fetch(input, init)
|
||||
},
|
||||
|
||||
getWslEnabled: async () => {
|
||||
const next = await window.api.getWslConfig().catch(() => null)
|
||||
if (next) return next.enabled
|
||||
return window.__KILO__!.wsl ?? false
|
||||
},
|
||||
getWslEnabled: () => isWslEnabled(),
|
||||
|
||||
setWslEnabled: async (enabled) => {
|
||||
await window.api.setWslConfig({ enabled })
|
||||
@@ -249,6 +253,7 @@ listenForDeepLinks()
|
||||
|
||||
render(() => {
|
||||
const platform = createPlatform()
|
||||
const [windowConfig] = createResource(() => window.api.getWindowConfig().catch(() => ({ updaterEnabled: false })))
|
||||
const loadLocale = async () => {
|
||||
const current = await platform.storage?.("opencode.global.dat").getItem("language")
|
||||
const legacy = current ? undefined : await platform.storage?.().getItem("language.v1")
|
||||
@@ -325,7 +330,15 @@ render(() => {
|
||||
return (
|
||||
<PlatformProvider value={platform}>
|
||||
<AppBaseProviders locale={locale.latest}>
|
||||
<Show when={!defaultServer.loading && !sidecar.loading && !windowCount.loading && !locale.loading}>
|
||||
<Show
|
||||
when={
|
||||
!defaultServer.loading &&
|
||||
!sidecar.loading &&
|
||||
!windowConfig.loading &&
|
||||
!windowCount.loading &&
|
||||
!locale.loading
|
||||
}
|
||||
>
|
||||
{(_) => {
|
||||
return (
|
||||
<AppInterface
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { initI18n, t } from "./i18n"
|
||||
|
||||
export const UPDATER_ENABLED = () => window.__KILO__?.updaterEnabled ?? false
|
||||
|
||||
export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) {
|
||||
await initI18n()
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="1.4.0" date="2026-04-08">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.4.0</url>
|
||||
</release>
|
||||
<release version="1.0.223" date="2026-01-01">
|
||||
<url type="details">https://github.com/Kilo-Org/kilocode/releases/tag/v1.0.223</url>
|
||||
</release>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"icons/dev/icon.ico"
|
||||
],
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"targets": ["deb", "rpm", "dmg", "nsis", "app"],
|
||||
"externalBin": ["sidecars/kilo-cli"],
|
||||
"linux": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.2.24"
|
||||
version = "7.2.26"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/Kilo-Org/kilocode"
|
||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.24/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.26/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.24/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.26/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.24/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.26/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.24/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.26/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.24/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.26/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -33,6 +33,11 @@ export const AutomateNav: NavSection[] = [
|
||||
href: "/automate/extending/shell-integration",
|
||||
children: "Shell Integration",
|
||||
},
|
||||
{
|
||||
href: "/automate/extending/plugins",
|
||||
children: "Plugins",
|
||||
platform: "new",
|
||||
},
|
||||
{
|
||||
href: "/automate/extending/auto-launch",
|
||||
children: "Auto-launch Configuration",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
---
|
||||
title: "Plugins"
|
||||
description: "Extend the Kilo CLI with custom hooks, tools, auth providers, and more"
|
||||
platform: new
|
||||
---
|
||||
|
||||
# Plugins
|
||||
|
||||
Plugins extend Kilo by hooking into events, adding custom tools, registering auth or model providers, and customizing runtime behavior. They are TypeScript or JavaScript modules loaded at startup, and work in both the Kilo CLI and the VS Code extension.
|
||||
|
||||
## What plugins can do
|
||||
|
||||
- **Add custom tools** the model can call (like `read`, `write`, `bash`).
|
||||
- **Intercept tool calls** to mutate arguments, rewrite output, or block dangerous operations.
|
||||
- **Subscribe to events** — sessions, messages, permissions, LSP diagnostics, file changes, etc.
|
||||
- **Register auth providers** — OAuth or API-key flows for model providers.
|
||||
- **Register model providers** — dynamic model catalogs.
|
||||
- **Mutate chat parameters or headers** sent to the LLM.
|
||||
- **Customize compaction** — inject or replace the prompt used when a session is compacted.
|
||||
- **Inject shell environment variables** for commands executed by the agent or user.
|
||||
|
||||
---
|
||||
|
||||
## Use a plugin
|
||||
|
||||
There are three ways to load plugins.
|
||||
|
||||
### From a config file
|
||||
|
||||
Add an array of plugin specifiers to your config file:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://app.kilo.ai/config.json",
|
||||
"plugin": [
|
||||
"@your-org/your-plugin",
|
||||
"your-plugin@1.2.3",
|
||||
["your-plugin", { "apiKey": "{env:MY_API_KEY}" }],
|
||||
"./plugins/local.ts",
|
||||
"file:///abs/path/plugin.ts"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each entry can be:
|
||||
|
||||
| Form | Loaded from |
|
||||
| -------------------------------------- | ---------------------------------------------------------------- |
|
||||
| `"package-name"` | Latest version from npm |
|
||||
| `"package-name@1.2.3"` | Pinned version from npm |
|
||||
| `["package-name", { options }]` | npm package with options passed to the plugin function |
|
||||
| `"./path/plugin.ts"` / `"file:///..."` | Local file (relative to the config file or absolute `file:` URL) |
|
||||
|
||||
Config files live in the same locations as the rest of your CLI configuration — see the [CLI configuration reference](/docs/code-with-ai/platforms/cli#configuration).
|
||||
|
||||
### From a plugin directory
|
||||
|
||||
Drop TypeScript or JavaScript files into a `plugin/` or `plugins/` folder inside any config directory:
|
||||
|
||||
- Global: `~/.config/kilo/plugin/`
|
||||
- Project: `.kilo/plugin/`, `.kilocode/plugin/`, or `.opencode/plugin/`
|
||||
|
||||
Every `.ts` or `.js` file in those directories is auto-registered at startup — no need to list them in the config file.
|
||||
|
||||
```text
|
||||
my-project/
|
||||
├── kilo.json
|
||||
└── .kilo/
|
||||
└── plugin/
|
||||
├── env-guard.ts
|
||||
└── notifications.ts
|
||||
```
|
||||
|
||||
### From the `kilo plugin` command
|
||||
|
||||
Install an npm plugin and patch your config in one step:
|
||||
|
||||
```bash
|
||||
# Install into the current project's config
|
||||
kilo plugin my-plugin
|
||||
|
||||
# Install into your global config
|
||||
kilo plugin my-plugin --global
|
||||
|
||||
# Replace an existing entry
|
||||
kilo plugin my-plugin --force
|
||||
```
|
||||
|
||||
The command resolves the package, reads its `package.json` for plugin entrypoints, and writes the entry into the appropriate config file (currently `.opencode/opencode.jsonc` / `.opencode/tui.jsonc` for local installs, or `~/.config/kilo/opencode.jsonc` / `~/.config/kilo/tui.jsonc` for `--global`) while preserving JSONC comments.
|
||||
|
||||
### How plugins are installed
|
||||
|
||||
- **npm plugins** are installed automatically at startup using Bun. Packages and their dependencies are cached in Kilo's XDG cache directory (`~/.cache/kilo/` on Linux, `~/Library/Caches/kilo/` on macOS, `%LOCALAPPDATA%\kilo\` on Windows).
|
||||
- **Local plugins** are loaded directly from the plugin directory. If your plugin imports external packages, add a `package.json` to your config directory (see [Dependencies](#dependencies)) — Kilo runs `bun install` on startup so imports resolve.
|
||||
|
||||
### Load order
|
||||
|
||||
Plugins from all sources run on every session. They load in this order:
|
||||
|
||||
1. Internal built-ins (Kilo Gateway auth, Codex auth, Copilot auth, Cloudflare, etc.)
|
||||
2. Global config plugin array (`~/.config/kilo/kilo.json`)
|
||||
3. Global plugin directory (`~/.config/kilo/plugin/`)
|
||||
4. Project config plugin array (`kilo.json` / `opencode.json`)
|
||||
5. Project plugin directory (`.kilo/plugin/` and friends)
|
||||
|
||||
Duplicates (same package, same version) are deduplicated. Hooks from multiple plugins run sequentially in load order.
|
||||
|
||||
### Disabling external plugins
|
||||
|
||||
Set the `KILO_PURE=1` environment variable to skip all external plugins — only built-in plugins will load. Useful for reproducible CI runs or debugging.
|
||||
|
||||
---
|
||||
|
||||
## Create a plugin
|
||||
|
||||
A plugin is a module that exports a function returning a set of [hooks](#hooks-reference).
|
||||
|
||||
### Basic structure
|
||||
|
||||
Create a file in your plugin directory:
|
||||
|
||||
```ts
|
||||
// .kilo/plugin/hello.ts
|
||||
import type { Plugin } from "@kilocode/plugin"
|
||||
|
||||
const hello: Plugin = async ({ project, client, $, directory, worktree }) => {
|
||||
console.log("hello plugin loaded")
|
||||
|
||||
return {
|
||||
// hook implementations go here
|
||||
}
|
||||
}
|
||||
|
||||
export default { id: "hello", server: hello }
|
||||
```
|
||||
|
||||
The plugin function receives a context object:
|
||||
|
||||
| Field | Description |
|
||||
| ------------------------ | ----------------------------------------------------------------- |
|
||||
| `project` | Current project metadata. |
|
||||
| `directory` | Current working directory for this session. |
|
||||
| `worktree` | Git worktree root for this session. |
|
||||
| `client` | A Kilo SDK client (`@kilocode/sdk`) for calling the local server. |
|
||||
| `$` | [Bun's shell API](https://bun.com/docs/runtime/shell). |
|
||||
| `serverUrl` | URL of the local Kilo server. |
|
||||
| `experimental_workspace` | Register workspace adaptors (used by Agent Manager). |
|
||||
|
||||
The function returns a `Hooks` object. Any second argument is the options object passed via config (e.g. the `{ apiKey: "..." }` from `["my-plugin", { apiKey: "..." }]`).
|
||||
|
||||
### Module shape
|
||||
|
||||
Plugins must default-export a module descriptor. `id` is required for local-file plugins and inferred from `package.json#name` for npm plugins.
|
||||
|
||||
```ts
|
||||
import type { Plugin } from "@kilocode/plugin"
|
||||
|
||||
const server: Plugin = async (ctx) => ({
|
||||
/* hooks */
|
||||
})
|
||||
|
||||
export default {
|
||||
id: "my-plugin",
|
||||
server,
|
||||
}
|
||||
```
|
||||
|
||||
An npm plugin can also expose a TUI entry point (`tui`) for [TUI plugins](#tui-plugins), but `server` and `tui` are separate modules.
|
||||
|
||||
### TypeScript support
|
||||
|
||||
Install the plugin package locally and import its types:
|
||||
|
||||
```bash
|
||||
bun add -d @kilocode/plugin
|
||||
```
|
||||
|
||||
```ts
|
||||
import type { Plugin } from "@kilocode/plugin"
|
||||
import { tool } from "@kilocode/plugin/tool"
|
||||
```
|
||||
|
||||
Kilo automatically creates a `package.json` in config directories that contain a `plugin/` folder and installs `@kilocode/plugin` so types resolve out of the box.
|
||||
|
||||
### Engine compatibility
|
||||
|
||||
Declare a CLI version range to prevent a plugin from loading against an incompatible build:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-plugin",
|
||||
"engines": { "opencode": "^7.0.0" }
|
||||
}
|
||||
```
|
||||
|
||||
If the running CLI does not satisfy the range, the plugin is skipped and a warning is surfaced.
|
||||
|
||||
### Dependencies
|
||||
|
||||
Local plugins and custom tools can use external npm packages. Add a `package.json` to your config directory:
|
||||
|
||||
```json
|
||||
// .kilo/package.json
|
||||
{
|
||||
"dependencies": {
|
||||
"shescape": "^2.1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Kilo runs `bun install` at startup so your plugins can import the packages:
|
||||
|
||||
```ts
|
||||
// .kilo/plugin/escape-bash.ts
|
||||
import { escape } from "shescape"
|
||||
import type { Plugin } from "@kilocode/plugin"
|
||||
|
||||
const EscapeBash: Plugin = async () => ({
|
||||
"tool.execute.before": async (input, output) => {
|
||||
if (input.tool === "bash") {
|
||||
output.args.command = escape(output.args.command)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default { id: "escape-bash", server: EscapeBash }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hooks reference
|
||||
|
||||
Every hook is optional. Return only the ones you care about.
|
||||
|
||||
### Lifecycle
|
||||
|
||||
| Hook | Description |
|
||||
| -------- | --------------------------------------------------------------------------------- |
|
||||
| `config` | Receives the fully-resolved config at startup. Read-only — useful for inspection. |
|
||||
| `event` | Called for **every** event on the internal bus (see [Events](#events)). |
|
||||
|
||||
### Tools
|
||||
|
||||
| Hook | Description |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `tool` | Map of tool name → [tool definition](#custom-tools). Added tools are callable by the model. |
|
||||
| `tool.execute.before` | Fires before a tool runs; you can mutate `output.args`. |
|
||||
| `tool.execute.after` | Fires after a tool returns; you can rewrite `output.title`, `output.output`, `output.metadata`. |
|
||||
| `tool.definition` | Mutate a tool's `description` and `parameters` before they are sent to the model. |
|
||||
|
||||
### Chat
|
||||
|
||||
| Hook | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------- |
|
||||
| `chat.message` | Fires when a new user message arrives. Inspect or modify `parts`. |
|
||||
| `chat.params` | Mutate `temperature`, `topP`, `topK`, `maxOutputTokens`, provider `options`. |
|
||||
| `chat.headers` | Add or replace HTTP headers on the LLM API call. |
|
||||
| `permission.ask` | Auto-allow or auto-deny permission prompts. |
|
||||
| `command.execute.before` | Intercept slash command execution; mutate the resulting `parts`. |
|
||||
| `shell.env` | Inject environment variables into every shell command Kilo runs. |
|
||||
|
||||
### Providers & auth
|
||||
|
||||
| Hook | Description |
|
||||
| ---------- | ------------------------------------------------------------------------------------ |
|
||||
| `auth` | Register an auth method (OAuth or API key) for a provider, with interactive prompts. |
|
||||
| `provider` | Dynamically supply a model catalog for a provider (useful for BYO-model gateways). |
|
||||
|
||||
### Experimental
|
||||
|
||||
These hooks live behind the `experimental.` prefix and may change between releases.
|
||||
|
||||
| Hook | Description |
|
||||
| -------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `experimental.chat.messages.transform` | Rewrite the full message history before it is sent to the model. |
|
||||
| `experimental.chat.system.transform` | Modify the system prompt array. |
|
||||
| `experimental.session.compacting` | Inject extra context (`output.context`) or replace the compaction prompt entirely (`output.prompt`). |
|
||||
| `experimental.compaction.autocontinue` | Disable the synthetic "continue" turn that follows compaction. |
|
||||
| `experimental.text.complete` | Post-process final text parts (e.g. append signatures, redact secrets). |
|
||||
|
||||
### Events
|
||||
|
||||
The `event` hook fires for every event on Kilo's internal bus. Common event types include:
|
||||
|
||||
- **Session**: `session.created`, `session.updated`, `session.idle`, `session.error`, `session.deleted`, `session.compacted`, `session.diff`, `session.status`
|
||||
- **Message**: `message.updated`, `message.removed`, `message.part.updated`, `message.part.removed`
|
||||
- **Tool**: `tool.execute.before`, `tool.execute.after`
|
||||
- **Permission**: `permission.asked`, `permission.replied`
|
||||
- **File**: `file.edited`, `file.watcher.updated`
|
||||
- **Shell**: `shell.env`
|
||||
- **Command**: `command.executed`
|
||||
- **LSP**: `lsp.updated`, `lsp.client.diagnostics`
|
||||
- **Todo**: `todo.updated`
|
||||
- **Server**: `server.connected`
|
||||
- **Installation**: `installation.updated`
|
||||
|
||||
```ts
|
||||
const server: Plugin = async () => ({
|
||||
event: async ({ event }) => {
|
||||
if (event.type === "session.idle") {
|
||||
// session finished responding
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom tools
|
||||
|
||||
Plugins can register tools the model can call alongside the built-in ones. Use the `tool()` helper for type-safety:
|
||||
|
||||
```ts
|
||||
// .kilo/plugin/database.ts
|
||||
import type { Plugin } from "@kilocode/plugin"
|
||||
import { tool } from "@kilocode/plugin/tool"
|
||||
|
||||
const DatabasePlugin: Plugin = async () => ({
|
||||
tool: {
|
||||
query: tool({
|
||||
description: "Run a read-only SQL query against the project database",
|
||||
args: {
|
||||
sql: tool.schema.string().describe("SQL query to execute"),
|
||||
},
|
||||
async execute(args, context) {
|
||||
const { directory, worktree } = context
|
||||
// your query logic here
|
||||
return `ran: ${args.sql}`
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export default { id: "database", server: DatabasePlugin }
|
||||
```
|
||||
|
||||
`args` uses a [Zod](https://zod.dev) schema via `tool.schema`. The tool's `execute` function receives:
|
||||
|
||||
- `args` — validated against your schema
|
||||
- `context` — `{ sessionID, messageID, agent, directory, worktree, abort, metadata, ask }`
|
||||
|
||||
### Name precedence
|
||||
|
||||
If a custom tool uses the same name as a built-in tool, **the custom tool wins**. Prefer unique names unless you intentionally want to override a built-in (for example, to wrap `bash` with extra validation).
|
||||
|
||||
### Alternative: standalone tool files
|
||||
|
||||
For tools that don't need the full plugin context, drop them in a `tool/` or `tools/` folder inside any config directory — for example `.kilo/tool/database.ts` or `~/.config/kilo/tool/database.ts`. The filename becomes the tool name, and each file exports a `tool()` definition directly. The layout is identical to the [OpenCode custom tools guide](https://opencode.ai/docs/custom-tools); substitute `.kilo/` (or `.kilocode/` / `.opencode/`) for `.opencode/`.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Send a notification when a session finishes
|
||||
|
||||
```ts
|
||||
// .kilo/plugin/notify.ts
|
||||
import type { Plugin } from "@kilocode/plugin"
|
||||
|
||||
const Notify: Plugin = async ({ $ }) => ({
|
||||
event: async ({ event }) => {
|
||||
if (event.type === "session.idle") {
|
||||
await $`osascript -e 'display notification "Session complete!" with title "Kilo"'`
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default { id: "notify", server: Notify }
|
||||
```
|
||||
|
||||
{% callout type="tip" %}
|
||||
The VS Code extension already emits system notifications when a session finishes or errors — this plugin is for the raw CLI / TUI.
|
||||
{% /callout %}
|
||||
|
||||
### Block reads of `.env` files
|
||||
|
||||
```ts
|
||||
// .kilo/plugin/env-guard.ts
|
||||
import type { Plugin } from "@kilocode/plugin"
|
||||
|
||||
const EnvGuard: Plugin = async () => ({
|
||||
"tool.execute.before": async (input, output) => {
|
||||
if (input.tool === "read" && String(output.args.filePath).includes(".env")) {
|
||||
throw new Error("reading .env files is blocked")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default { id: "env-guard", server: EnvGuard }
|
||||
```
|
||||
|
||||
### Inject environment variables into every shell command
|
||||
|
||||
```ts
|
||||
// .kilo/plugin/inject-env.ts
|
||||
import type { Plugin } from "@kilocode/plugin"
|
||||
|
||||
const InjectEnv: Plugin = async () => ({
|
||||
"shell.env": async (input, output) => {
|
||||
output.env.MY_API_KEY = "secret"
|
||||
output.env.PROJECT_ROOT = input.cwd
|
||||
},
|
||||
})
|
||||
|
||||
export default { id: "inject-env", server: InjectEnv }
|
||||
```
|
||||
|
||||
### Structured logging
|
||||
|
||||
Prefer `client.app.log()` over `console.log` so entries land in Kilo's log pipeline:
|
||||
|
||||
```ts
|
||||
import type { Plugin } from "@kilocode/plugin"
|
||||
|
||||
const Logger: Plugin = async ({ client }) => {
|
||||
await client.app.log({
|
||||
body: {
|
||||
service: "my-plugin",
|
||||
level: "info",
|
||||
message: "plugin initialized",
|
||||
extra: { version: "1.0.0" },
|
||||
},
|
||||
})
|
||||
return {}
|
||||
}
|
||||
|
||||
export default { id: "logger", server: Logger }
|
||||
```
|
||||
|
||||
Levels: `debug`, `info`, `warn`, `error`.
|
||||
|
||||
### Inject context during session compaction
|
||||
|
||||
```ts
|
||||
// .kilo/plugin/compaction.ts
|
||||
import type { Plugin } from "@kilocode/plugin"
|
||||
|
||||
const Compaction: Plugin = async () => ({
|
||||
"experimental.session.compacting": async (input, output) => {
|
||||
output.context.push(
|
||||
"## Persist across compaction\n- current task status\n- files being actively edited\n- key decisions",
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default { id: "compaction", server: Compaction }
|
||||
```
|
||||
|
||||
Set `output.prompt` to replace the default compaction prompt entirely — when present, `output.context` is ignored.
|
||||
|
||||
---
|
||||
|
||||
## TUI plugins
|
||||
|
||||
Plugins can also target the Kilo TUI itself — registering slash commands, routes, sidebar slots, dialogs, and keybinds. TUI plugins are SolidJS modules exported from `"./tui"` in your plugin package.
|
||||
|
||||
TUI plugins live in a separate module namespace (`@kilocode/plugin/tui`) and have their own API surface (`TuiPluginApi`). Because the TUI API is larger and still evolving, this guide doesn't cover it exhaustively — use the types in `@kilocode/plugin/tui` as the reference, and look at the built-in TUI plugins under `packages/opencode/src/cli/cmd/tui/feature-plugins/` for working examples.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Plugin failed to load** — check the CLI logs with `kilo --print-logs --log-level DEBUG`. Load failures are also surfaced as session errors in the TUI and VS Code extension.
|
||||
- **Plugin loaded but hooks never fire** — make sure the default export includes `server`:
|
||||
|
||||
```ts
|
||||
export default { id: "my-plugin", server }
|
||||
```
|
||||
|
||||
Named function exports are also accepted for backwards compatibility but should be considered legacy.
|
||||
|
||||
- **Local plugin can't find an npm import** — add a `package.json` in the config directory so `bun install` picks up the dependency (see [Dependencies](#dependencies)).
|
||||
- **Plugin loads in dev but not in CI** — verify `KILO_PURE` is not set, and that npm-installed plugins are cached (Kilo's XDG cache directory — `~/.cache/kilo/` on Linux, `~/Library/Caches/kilo/` on macOS, `%LOCALAPPDATA%\kilo\` on Windows). Run with `--log-level DEBUG` to see install output.
|
||||
- **Reset the plugin cache** — delete the `node_modules/` under Kilo's cache directory (or the `node_modules` cache under your config directory) and restart Kilo.
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
- Types: [`@kilocode/plugin`](https://github.com/Kilo-Org/kilocode/tree/main/packages/plugin) — `Plugin`, `Hooks`, `PluginInput`, `ToolDefinition`, `AuthHook`, `ProviderHook`.
|
||||
- Example plugin: [`packages/plugin/src/example.ts`](https://github.com/Kilo-Org/kilocode/blob/main/packages/plugin/src/example.ts)
|
||||
- CLI command: [`kilo plugin`](/docs/code-with-ai/platforms/cli-reference#kilo-plugin)
|
||||
- Upstream docs (behavior is identical to OpenCode): [opencode.ai/docs/plugins](https://opencode.ai/docs/plugins) and [opencode.ai/docs/custom-tools](https://opencode.ai/docs/custom-tools)
|
||||
@@ -20,12 +20,13 @@ When you describe a task, the agent uses its tools — `read`, `grep`, `glob`, a
|
||||
|
||||
Type `@` in the chat input to get autocomplete suggestions. You can mention:
|
||||
|
||||
| Mention | Description | Example |
|
||||
| ------------ | ------------------------------------------- | --------------- |
|
||||
| **File** | Attach a file's contents to your message | `@src/utils.ts` |
|
||||
| **Terminal** | Include your active VS Code terminal output | `@terminal` |
|
||||
| Mention | Description | Example |
|
||||
| ---------------- | ----------------------------------------------------- | --------------- |
|
||||
| **File** | Attach a file's contents to your message | `@src/utils.ts` |
|
||||
| **Terminal** | Include your active VS Code terminal output | `@terminal` |
|
||||
| **Git Changes** | Attach uncommitted working-tree diffs and new files | `@git-changes` |
|
||||
|
||||
Selecting a suggestion inserts the mention and highlights it in the input. File contents and terminal output are attached as context when you send the message.
|
||||
Selecting a suggestion inserts the mention and highlights it in the input. File contents, terminal output, and git changes are attached as context when you send the message.
|
||||
|
||||
### Drag and Drop
|
||||
|
||||
|
||||
@@ -12,7 +12,12 @@ Kilo Code's autocomplete feature provides intelligent code suggestions and compl
|
||||
|
||||
## How Autocomplete Works
|
||||
|
||||
The extension uses **Fill-in-the-Middle (FIM)** completion powered by Codestral (`mistralai/codestral-2508`) via the **Kilo Gateway**. It analyzes the code before and after your cursor to generate contextually accurate inline suggestions.
|
||||
The extension uses **Fill-in-the-Middle (FIM)** completion routed through the **Kilo Gateway**. It analyzes the code before and after your cursor to generate contextually accurate inline suggestions.
|
||||
|
||||
You can choose between two FIM models:
|
||||
|
||||
- **Codestral** (`mistralai/codestral-2508`) by Mistral AI — the default, billed through your Kilo account.
|
||||
- **Mercury Edit** (`inception/mercury-edit`) by Inception — temporarily available via **BYOK** (Bring Your Own Key) only; Kilo Gateway support is coming soon.
|
||||
|
||||
## Triggering Options
|
||||
|
||||
@@ -30,9 +35,14 @@ This keybinding requires `kilo-code.new.autocomplete.enableSmartInlineTaskKeybin
|
||||
|
||||
## Provider and Model
|
||||
|
||||
Autocomplete currently uses **Codestral** (`mistralai/codestral-2508`) routed through the **Kilo Gateway**. Codestral is optimized for Fill-in-the-Middle (FIM) completions, and there is no option to select a different model at this time. Support for additional FIM models is planned for future releases.
|
||||
Autocomplete requests are routed through the **Kilo Gateway**. You can pick the FIM model under **Settings → Models → Autocomplete model**:
|
||||
|
||||
Requests are billed through your Kilo account. To use your own Mistral API key instead, see [Setting Up Mistral for Free Autocomplete](/docs/code-with-ai/features/autocomplete/mistral-setup).
|
||||
- **Codestral** (`mistralai/codestral-2508`) — the default. Billed through your Kilo account, or free when you add your own Mistral Codestral key via BYOK. See [Setting Up Mistral for Free Autocomplete](/docs/code-with-ai/features/autocomplete/mistral-setup).
|
||||
- **Mercury Edit** (`inception/mercury-edit`) — a fast diffusion-based FIM model by Inception. Temporarily requires an **Inception BYOK key** until Kilo Gateway support lands. Add one from the [BYOK page](https://app.kilo.ai/byok) in the Kilo platform. See [Bring Your Own Key (BYOK)](/docs/getting-started/byok) for setup details.
|
||||
|
||||
{% callout type="note" %}
|
||||
Mercury Edit is only available through BYOK for now — Kilo Gateway support is coming soon. If you select Mercury Edit without a valid Inception BYOK key configured, autocomplete requests will fail — switch back to Codestral or add an Inception key to continue.
|
||||
{% /callout %}
|
||||
|
||||
## Status Bar
|
||||
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c67c86c68c4a07514db50c964668753c0a85ec410e55208b6e094f711c0bceb5
|
||||
size 20317
|
||||
oid sha256:9c87ac5179843ad5dadf73023f87d1f0b0589a81420e9ef406534a335598a0a2
|
||||
size 28082
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4524d1fe8b44cf2cce15bdf3ad100572b57fa479fefe456f1c141ebca8d5187d
|
||||
size 28078
|
||||
oid sha256:16e602e6a870e4b66a466a0895f92e9df117f6137809c79ed797f24d2b6a1e88
|
||||
size 31028
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Source Code Links
|
||||
|
||||
<!-- Auto-generated by script/extract-source-links.ts — DO NOT EDIT -->
|
||||
<!-- 81 unique URLs extracted from extension and CLI source -->
|
||||
<!-- 82 unique URLs extracted from extension and CLI source -->
|
||||
|
||||
- <https://api.apertis.ai/v1>
|
||||
<!-- packages/opencode/src/provider/model-cache.ts -->
|
||||
@@ -16,6 +16,7 @@
|
||||
<!-- packages/opencode/src/kilocode/components/dialog-claw-setup.tsx -->
|
||||
<!-- packages/opencode/src/kilocode/components/dialog-claw-upgrade.tsx -->
|
||||
- <https://app.kilo.ai/config.json>
|
||||
<!-- packages/kilo-vscode/src/kilo-provider/config-file.ts -->
|
||||
<!-- packages/opencode/src/config/config.ts -->
|
||||
- <https://app.kilo.ai/credits>
|
||||
<!-- packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts -->
|
||||
@@ -33,6 +34,8 @@
|
||||
<!-- packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts -->
|
||||
- <https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services>
|
||||
<!-- packages/opencode/src/cli/cmd/github.ts -->
|
||||
- <https://docs.mistral.ai/capabilities/reasoning/adjustable>
|
||||
<!-- packages/opencode/src/provider/transform.ts -->
|
||||
- <https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort>
|
||||
<!-- packages/opencode/src/provider/transform.ts -->
|
||||
- <https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||
@@ -33,7 +33,7 @@
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
"@ai-sdk/openai-compatible": "2.0.37",
|
||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||
"@openrouter/ai-sdk-provider": "2.8.1",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"ai": "catalog:",
|
||||
"open": "10.1.2",
|
||||
|
||||
@@ -112,6 +112,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
|
||||
content: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
text: z.string().optional(), // Text-completion style streaming (Mercury)
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Kilo-specific i18n translations and overrides",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -2022,11 +2022,28 @@ ToolRegistry.register({
|
||||
const filename = () => getFilename(props.input.filePath ?? "")
|
||||
const pending = () => busy(props.status)
|
||||
const reveal = useToolReveal(pending, () => props.reveal !== false)
|
||||
const before = () => props.metadata?.filediff?.before ?? props.input.oldString ?? ""
|
||||
const after = () => props.metadata?.filediff?.after ?? props.input.newString ?? ""
|
||||
const canOpenDiff = () => !!data.openDiff && !!path() && (before() !== "" || after() !== "")
|
||||
const canOpenFile = () => !!data.openFile && !!path()
|
||||
|
||||
const handleFileClick = (e: MouseEvent) => {
|
||||
if (!data.openFile || !props.input.filePath) return
|
||||
e.stopPropagation()
|
||||
data.openFile(props.input.filePath)
|
||||
|
||||
if (canOpenDiff()) {
|
||||
data.openDiff!({
|
||||
file: path(),
|
||||
before: before(),
|
||||
after: after(),
|
||||
additions: props.metadata?.filediff?.additions ?? 0,
|
||||
deletions: props.metadata?.filediff?.deletions ?? 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (canOpenFile()) {
|
||||
data.openFile!(path())
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -2049,7 +2066,7 @@ ToolRegistry.register({
|
||||
path={props.input.filePath?.includes("/") ? getDirectory(props.input.filePath!) : undefined}
|
||||
changes={props.metadata.filediff}
|
||||
animate={reveal()}
|
||||
onClick={data.openFile && props.input.filePath ? handleFileClick : undefined}
|
||||
onClick={canOpenDiff() || canOpenFile() ? handleFileClick : undefined}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
@@ -2072,12 +2089,12 @@ ToolRegistry.register({
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
before={{
|
||||
name: props.metadata?.filediff?.file || props.input.filePath,
|
||||
contents: props.metadata?.filediff?.before || props.input.oldString,
|
||||
name: path(),
|
||||
contents: before(),
|
||||
}}
|
||||
after={{
|
||||
name: props.metadata?.filediff?.file || props.input.filePath,
|
||||
contents: props.metadata?.filediff?.after || props.input.newString,
|
||||
name: path(),
|
||||
contents: after(),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# kilo-code
|
||||
|
||||
## 7.2.26
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#9310](https://github.com/Kilo-Org/kilocode/pull/9310) [`f067a90`](https://github.com/Kilo-Org/kilocode/commit/f067a908a9fe161cfb87298b593b73e7e9bdb0b6) - Support selecting Mercury Edit by Inception for autocomplete.
|
||||
|
||||
- [#9548](https://github.com/Kilo-Org/kilocode/pull/9548) [`c5614cc`](https://github.com/Kilo-Org/kilocode/commit/c5614cc54ed16e90151cbe1ceed5213b472383ff) - Add Settings header buttons to open the project and global Kilo config files directly in VS Code.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#9552](https://github.com/Kilo-Org/kilocode/pull/9552) [`3f0bf32`](https://github.com/Kilo-Org/kilocode/commit/3f0bf322c1e2397cffaf22739df8d41e1957f5ba) - Fix clearing an agent's Model Override in Agent Behaviour settings. Previously, clearing the field and saving would repopulate the old value because the empty input was sent as `undefined` and dropped by `JSON.stringify`, so the backend never received a delete instruction. The field now reverts to the global default model as expected.
|
||||
|
||||
- [#9551](https://github.com/Kilo-Org/kilocode/pull/9551) [`b344ac9`](https://github.com/Kilo-Org/kilocode/commit/b344ac97c6a2d7caecf85b43b8c6a07ccb352b6b) - Restore disabled provider management in the VS Code extension provider settings.
|
||||
|
||||
## 7.2.25
|
||||
|
||||
## 7.2.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
"name": "kilo-code",
|
||||
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
|
||||
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
|
||||
"version": "7.2.24",
|
||||
"version": "7.2.26",
|
||||
"icon": "assets/icons/logo-outline-black.png",
|
||||
"galleryBanner": {
|
||||
"color": "#FFFFFF",
|
||||
@@ -61,6 +61,15 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"icons": {
|
||||
"kilo-logo": {
|
||||
"description": "Kilo Code logo",
|
||||
"default": {
|
||||
"fontPath": "assets/icons/kilo-icon-font.woff2",
|
||||
"fontCharacter": "\\F101"
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewsContainers": {
|
||||
"activitybar": [
|
||||
{
|
||||
@@ -740,6 +749,19 @@
|
||||
"default": "kilo-auto/free",
|
||||
"description": "Default model ID for new sessions"
|
||||
},
|
||||
"kilo-code.new.autocomplete.model": {
|
||||
"type": "string",
|
||||
"default": "mistralai/codestral-2508",
|
||||
"enum": [
|
||||
"mistralai/codestral-2508",
|
||||
"inception/mercury-edit"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Codestral by Mistral AI (default)",
|
||||
"Mercury Edit by Inception"
|
||||
],
|
||||
"description": "Model to use for inline autocomplete suggestions"
|
||||
},
|
||||
"kilo-code.new.autocomplete.enableAutoTrigger": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
} from "./services/autocomplete/settings"
|
||||
import * as ModelState from "./kilo-provider/model-state"
|
||||
import { handleForkSession } from "./kilo-provider/fork-session"
|
||||
import { openConfig } from "./kilo-provider/open-config"
|
||||
import { retryable, backoff, MAX_RETRIES } from "./util/retry"
|
||||
import { hasGit } from "./kilo-provider/git-status"
|
||||
// legacy-migration start
|
||||
@@ -697,6 +698,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
case "openVSCodeSettings":
|
||||
vscode.commands.executeCommand("workbench.action.openSettings", message.query)
|
||||
break
|
||||
case "openConfigFile":
|
||||
await openConfig(message.scope, message.labels, this.getProjectDirectory(this.currentSession?.id))
|
||||
break
|
||||
case "openMarketplacePanel":
|
||||
vscode.commands.executeCommand("kilo-code.new.marketplaceButtonClicked", this.projectDirectory)
|
||||
break
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { existsSync } from "fs"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
|
||||
export type Scope = "global" | "local"
|
||||
|
||||
export type Source =
|
||||
| "sourceXdg"
|
||||
| "sourceHomeKilo"
|
||||
| "sourceHomeKilocode"
|
||||
| "sourceHomeOpencode"
|
||||
| "sourceEnvFile"
|
||||
| "sourceEnvDir"
|
||||
| "sourceEnvContent"
|
||||
| "sourceProjectKilo"
|
||||
| "sourceProjectRoot"
|
||||
| "sourceProjectKilocode"
|
||||
| "sourceProjectOpencode"
|
||||
|
||||
export interface Entry {
|
||||
file?: string
|
||||
name: string
|
||||
source: Source
|
||||
exists: boolean
|
||||
loaded: boolean
|
||||
legacy?: boolean
|
||||
recommended?: boolean
|
||||
virtual?: boolean
|
||||
}
|
||||
|
||||
const SCHEMA = "https://app.kilo.ai/config.json"
|
||||
|
||||
const MODERN = ["kilo.jsonc", "kilo.json"]
|
||||
const LEGACY = ["opencode.jsonc", "opencode.json"]
|
||||
const FILES = [...MODERN, ...LEGACY]
|
||||
const GLOBAL = ["kilo.jsonc", "kilo.json", "opencode.jsonc", "opencode.json", "config.json"]
|
||||
const HOME = [".kilo", ".kilocode", ".opencode"]
|
||||
const SOURCES: Record<string, Source> = {
|
||||
".kilo": "sourceHomeKilo",
|
||||
".kilocode": "sourceHomeKilocode",
|
||||
".opencode": "sourceHomeOpencode",
|
||||
}
|
||||
|
||||
function row(file: string, source: Source, loaded = true, recommended = false): Entry {
|
||||
const name = path.basename(file)
|
||||
return {
|
||||
file,
|
||||
name,
|
||||
source,
|
||||
exists: existsSync(file),
|
||||
loaded: loaded && existsSync(file),
|
||||
legacy: name.startsWith("opencode") || name === "config.json" || file.includes(`${path.sep}.kilocode${path.sep}`),
|
||||
recommended,
|
||||
}
|
||||
}
|
||||
|
||||
function ensure(list: Entry[], file: string, source: Source) {
|
||||
if (list.some((item) => item.file === file)) return list
|
||||
return [...list, row(file, source, true, true)]
|
||||
}
|
||||
|
||||
export function globalFiles() {
|
||||
const root = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "kilo")
|
||||
const base = GLOBAL.map((file) => row(path.join(root, file), "sourceXdg")).filter((item) => item.exists)
|
||||
const dirs = HOME.flatMap((dir) => {
|
||||
const base = path.join(os.homedir(), dir)
|
||||
if (!existsSync(base)) return []
|
||||
return FILES.map((file) => row(path.join(base, file), SOURCES[dir])).filter((item) => item.exists)
|
||||
})
|
||||
const env = process.env.KILO_CONFIG ? [row(process.env.KILO_CONFIG, "sourceEnvFile")] : []
|
||||
const extra = process.env.KILO_CONFIG_DIR
|
||||
const dir = extra
|
||||
? ensure(
|
||||
FILES.map((file) => row(path.join(extra, file), "sourceEnvDir")).filter((item) => item.exists),
|
||||
path.join(extra, "kilo.jsonc"),
|
||||
"sourceEnvDir",
|
||||
)
|
||||
: []
|
||||
const virtual: Entry[] = process.env.KILO_CONFIG_CONTENT
|
||||
? [
|
||||
{
|
||||
name: "KILO_CONFIG_CONTENT",
|
||||
source: "sourceEnvContent",
|
||||
exists: true,
|
||||
loaded: true,
|
||||
virtual: true,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
return ensure([...base, ...dirs, ...env, ...dir, ...virtual], path.join(root, "kilo.jsonc"), "sourceXdg")
|
||||
}
|
||||
|
||||
export function localFiles(root: string) {
|
||||
const enabled = !process.env.KILO_DISABLE_PROJECT_CONFIG
|
||||
const dirs = [path.join(root, ".kilo"), root, path.join(root, ".kilocode"), path.join(root, ".opencode")]
|
||||
const list = dirs.flatMap((dir) => FILES.map((file) => row(path.join(dir, file), localSource(root, dir), enabled)))
|
||||
return ensure(
|
||||
list.filter((item) => item.exists),
|
||||
path.join(root, ".kilo", "kilo.jsonc"),
|
||||
"sourceProjectKilo",
|
||||
).map((item) => (enabled ? item : { ...item, loaded: false }))
|
||||
}
|
||||
|
||||
function localSource(root: string, dir: string) {
|
||||
if (dir === root) return "sourceProjectRoot"
|
||||
if (dir.endsWith(`${path.sep}.kilo`)) return "sourceProjectKilo"
|
||||
if (dir.endsWith(`${path.sep}.kilocode`)) return "sourceProjectKilocode"
|
||||
return "sourceProjectOpencode"
|
||||
}
|
||||
|
||||
export function content() {
|
||||
return `{
|
||||
"$schema": "${SCHEMA}"
|
||||
}
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { content, globalFiles, localFiles, type Entry, type Scope, type Source } from "./config-file"
|
||||
|
||||
interface Labels extends Record<Source, string> {
|
||||
scope: string
|
||||
statusLoaded: string
|
||||
statusLoadedLegacy: string
|
||||
statusNotLoaded: string
|
||||
statusCreate: string
|
||||
title: string
|
||||
placeholder: string
|
||||
noWorkspace: string
|
||||
openFailed: string
|
||||
}
|
||||
|
||||
export async function openConfig(scope: Scope, labels: Labels, root?: string): Promise<void> {
|
||||
if (scope === "local" && !root) {
|
||||
void vscode.window.showWarningMessage(labels.noWorkspace)
|
||||
return
|
||||
}
|
||||
|
||||
const list = scope === "global" ? globalFiles() : localFiles(root!)
|
||||
const picked = await pick(list, labels)
|
||||
if (!picked?.file) return
|
||||
|
||||
await open(picked.file, labels)
|
||||
}
|
||||
|
||||
async function pick(list: Entry[], labels: Labels) {
|
||||
const editable = list.filter((item) => !item.virtual)
|
||||
if (editable.length === 1) return editable[0]
|
||||
|
||||
const picked = await vscode.window.showQuickPick(
|
||||
editable.map((item) => ({
|
||||
label: item.recommended && !item.exists ? `$(add) ${item.name}` : `$(json) ${item.name}`,
|
||||
description: item.exists ? status(item, labels) : labels.statusCreate,
|
||||
detail: `${labels[item.source]} - ${item.file}`,
|
||||
item,
|
||||
})),
|
||||
{
|
||||
title: labels.title,
|
||||
placeHolder: labels.placeholder,
|
||||
},
|
||||
)
|
||||
|
||||
return picked?.item
|
||||
}
|
||||
|
||||
function status(item: Entry, labels: Labels) {
|
||||
if (!item.loaded) return labels.statusNotLoaded
|
||||
if (item.legacy) return labels.statusLoadedLegacy
|
||||
return labels.statusLoaded
|
||||
}
|
||||
|
||||
async function open(file: string, labels: Labels) {
|
||||
const uri = vscode.Uri.file(file)
|
||||
try {
|
||||
await vscode.workspace.fs.createDirectory(vscode.Uri.file(path.dirname(file)))
|
||||
const exists = await vscode.workspace.fs.stat(uri).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
if (!exists) await vscode.workspace.fs.writeFile(uri, Buffer.from(content()))
|
||||
const doc = await vscode.workspace.openTextDocument(uri)
|
||||
await vscode.window.showTextDocument(doc, { preview: false })
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
console.error("[Kilo New] Failed to open config file:", file, err)
|
||||
void vscode.window.showErrorMessage(labels.openFailed.replace("{{message}}", () => msg))
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ import { KILO_AUTO, parseModelString } from "./shared/provider-model"
|
||||
*/
|
||||
type AuthState = "api" | "oauth" | "wellknown"
|
||||
|
||||
function disabledWithout(list: string[] | undefined, id: string) {
|
||||
return (list ?? []).filter((item) => item !== id)
|
||||
}
|
||||
|
||||
/** Fetch auth methods alongside the provider list. Auth states default to empty (endpoint not yet available). */
|
||||
export async function fetchProviderData(client: KiloClient, dir: string) {
|
||||
const authRequest =
|
||||
@@ -232,6 +236,9 @@ export async function disconnectProvider(
|
||||
try {
|
||||
const globalConfig = (await ctx.client.global.config.get({ throwOnError: true })).data ?? {}
|
||||
const configured = !!globalConfig.provider?.[id]
|
||||
const { response } = await fetchProviderData(ctx.client, ctx.workspaceDir)
|
||||
const active = response.all.find((item) => item.id === id)
|
||||
const oauth = active?.source === "custom" && configured
|
||||
|
||||
// Remove auth store entry. Config-sourced providers may not have an auth
|
||||
// store entry (credentials come from config or env), so failure is non-fatal.
|
||||
@@ -251,7 +258,7 @@ export async function disconnectProvider(
|
||||
// server rebuilds state from config. Add to disabled_providers so the server
|
||||
// excludes them. The config entry is preserved (user may re-enable later).
|
||||
// This matches the desktop app's disableProvider() pattern.
|
||||
if (configured) {
|
||||
if (configured && !oauth) {
|
||||
const disabled = globalConfig.disabled_providers ?? []
|
||||
if (!disabled.includes(id)) {
|
||||
const merged = (
|
||||
@@ -267,6 +274,19 @@ export async function disconnectProvider(
|
||||
}
|
||||
}
|
||||
|
||||
if (oauth) {
|
||||
const disabled = disabledWithout(globalConfig.disabled_providers, id)
|
||||
if (disabled.length !== (globalConfig.disabled_providers ?? []).length) {
|
||||
const merged = (
|
||||
await ctx.client.global.config.update({ config: { disabled_providers: disabled } }, { throwOnError: true })
|
||||
).data
|
||||
if (merged) {
|
||||
setCachedConfig({ type: "configLoaded", config: merged })
|
||||
ctx.postMessage({ type: "configUpdated", config: merged })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.disposeGlobal(`provider disconnect (${id})`)
|
||||
await ctx.fetchAndSendProviders()
|
||||
ctx.postMessage({ type: "providerDisconnected", requestId, providerID: id })
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { ResponseMetaData } from "./types"
|
||||
import type { KiloConnectionService } from "../cli-backend"
|
||||
|
||||
const DEFAULT_MODEL = "mistralai/codestral-2508"
|
||||
const PROVIDER_DISPLAY_NAME = "Kilo Gateway"
|
||||
import { DEFAULT_AUTOCOMPLETE_MODEL, getAutocompleteModel } from "../../shared/autocomplete-models"
|
||||
|
||||
export class AutocompleteModel {
|
||||
private connectionService: KiloConnectionService | null = null
|
||||
private currentModel: string = DEFAULT_AUTOCOMPLETE_MODEL.id
|
||||
public profileName: string | null = null
|
||||
public profileType: string | null = null
|
||||
|
||||
@@ -15,6 +14,10 @@ export class AutocompleteModel {
|
||||
}
|
||||
}
|
||||
|
||||
public setModel(model: string): void {
|
||||
this.currentModel = model
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection service (can be called after construction when service becomes available)
|
||||
*/
|
||||
@@ -48,13 +51,16 @@ export class AutocompleteModel {
|
||||
// client catches HTTP errors (402, 401, 429, 5xx) internally and silently
|
||||
// ends the stream. Without this, errors never reach ErrorBackoff.
|
||||
let sseError: Error | undefined
|
||||
|
||||
const temp = getAutocompleteModel(this.currentModel).temperature
|
||||
|
||||
const { stream } = await client.kilo.fim(
|
||||
{
|
||||
prefix,
|
||||
suffix,
|
||||
model: DEFAULT_MODEL,
|
||||
model: this.currentModel,
|
||||
maxTokens: 256,
|
||||
temperature: 0.2,
|
||||
temperature: temp,
|
||||
},
|
||||
{
|
||||
signal,
|
||||
@@ -66,7 +72,8 @@ export class AutocompleteModel {
|
||||
)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices?.[0]?.delta?.content
|
||||
const choice = chunk.choices?.[0]
|
||||
const content = choice?.delta?.content ?? choice?.text
|
||||
if (content) onChunk(content)
|
||||
if (chunk.usage) {
|
||||
inputTokens = chunk.usage.prompt_tokens ?? 0
|
||||
@@ -87,11 +94,11 @@ export class AutocompleteModel {
|
||||
}
|
||||
|
||||
public getModelName(): string {
|
||||
return DEFAULT_MODEL
|
||||
return this.currentModel
|
||||
}
|
||||
|
||||
public getProviderDisplayName(): string {
|
||||
return PROVIDER_DISPLAY_NAME
|
||||
return getAutocompleteModel(this.currentModel).provider
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AutocompleteCodeActionProvider } from "./AutocompleteCodeActionProvider
|
||||
import { AutocompleteInlineCompletionProvider } from "./classic-auto-complete/AutocompleteInlineCompletionProvider"
|
||||
import { AutocompleteTelemetry } from "./classic-auto-complete/AutocompleteTelemetry"
|
||||
import type { KiloConnectionService } from "../cli-backend"
|
||||
import { DEFAULT_AUTOCOMPLETE_MODEL } from "../../shared/autocomplete-models"
|
||||
|
||||
const CONFIG_SECTION = "kilo-code.new.autocomplete"
|
||||
|
||||
@@ -26,6 +27,7 @@ function readSettings(): AutocompleteServiceSettings {
|
||||
enableAutoTrigger: config.get<boolean>("enableAutoTrigger") ?? true,
|
||||
enableSmartInlineTaskKeybinding: config.get<boolean>("enableSmartInlineTaskKeybinding") ?? true,
|
||||
enableChatAutocomplete: config.get<boolean>("enableChatAutocomplete") ?? true,
|
||||
model: config.get<string>("model") ?? DEFAULT_AUTOCOMPLETE_MODEL.id,
|
||||
snoozeUntil: config.get<number>("snoozeUntil"),
|
||||
}
|
||||
}
|
||||
@@ -118,6 +120,10 @@ export class AutocompleteServiceManager {
|
||||
public async load() {
|
||||
this.settings = readSettings()
|
||||
|
||||
if (this.settings.model) {
|
||||
this.model.setModel(this.settings.model)
|
||||
}
|
||||
|
||||
await this.updateGlobalContext()
|
||||
this.updateStatusBar()
|
||||
await this.ensureInlineCompletionProviderRegistration()
|
||||
|
||||
@@ -83,9 +83,16 @@ describe("AutocompleteModel", () => {
|
||||
})
|
||||
|
||||
describe("getProviderDisplayName", () => {
|
||||
it("returns Kilo Gateway", () => {
|
||||
it("returns the default provider", () => {
|
||||
const model = new AutocompleteModel()
|
||||
expect(model.getProviderDisplayName()).toBe("Kilo Gateway")
|
||||
expect(model.getProviderDisplayName()).toBe("Mistral AI")
|
||||
})
|
||||
|
||||
it("returns the selected provider", () => {
|
||||
const model = new AutocompleteModel()
|
||||
model.setModel("inception/mercury-edit")
|
||||
|
||||
expect(model.getProviderDisplayName()).toBe("Inception")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -136,6 +143,23 @@ describe("AutocompleteModel", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("streams text-completion chunks", async () => {
|
||||
const chunks = [{ choices: [{ text: "hello" }] }, { choices: [{ text: " world" }] }]
|
||||
|
||||
const connection = createMockConnectionService("connected")
|
||||
mockClient.kilo.fim.mockResolvedValue({
|
||||
stream: (async function* () {
|
||||
for (const chunk of chunks) yield chunk
|
||||
})(),
|
||||
})
|
||||
|
||||
const model = new AutocompleteModel(connection)
|
||||
const received: string[] = []
|
||||
await model.generateFimResponse("prefix", "suffix", (text) => received.push(text))
|
||||
|
||||
expect(received).toEqual(["hello", " world"])
|
||||
})
|
||||
|
||||
it("passes model parameters to fim call", async () => {
|
||||
const connection = createMockConnectionService("connected")
|
||||
mockClient.kilo.fim.mockResolvedValue({
|
||||
@@ -157,5 +181,24 @@ describe("AutocompleteModel", () => {
|
||||
expect.objectContaining({ signal }),
|
||||
)
|
||||
})
|
||||
|
||||
it("passes selected model parameters to fim call", async () => {
|
||||
const connection = createMockConnectionService("connected")
|
||||
mockClient.kilo.fim.mockResolvedValue({
|
||||
stream: (async function* () {})(),
|
||||
})
|
||||
|
||||
const model = new AutocompleteModel(connection)
|
||||
model.setModel("inception/mercury-edit")
|
||||
await model.generateFimResponse("pre", "suf", vi.fn())
|
||||
|
||||
expect(mockClient.kilo.fim).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: "inception/mercury-edit",
|
||||
temperature: 0,
|
||||
}),
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const state = new Map<string, unknown>()
|
||||
const update = vi.fn(async (key: string, value: unknown) => {
|
||||
state.set(key, value)
|
||||
})
|
||||
|
||||
vi.mock("vscode", () => ({
|
||||
ConfigurationTarget: {
|
||||
Global: 1,
|
||||
},
|
||||
workspace: {
|
||||
getConfiguration: vi.fn(() => ({
|
||||
get: vi.fn((key: string, fallback: unknown) => state.get(key) ?? fallback),
|
||||
update,
|
||||
})),
|
||||
onDidChangeConfiguration: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe("autocomplete settings", () => {
|
||||
beforeEach(() => {
|
||||
state.clear()
|
||||
update.mockClear()
|
||||
})
|
||||
|
||||
it("includes the configured model in loaded settings", async () => {
|
||||
state.set("model", "inception/mercury-edit")
|
||||
const { buildAutocompleteSettingsMessage } = await import("../settings")
|
||||
|
||||
expect(buildAutocompleteSettingsMessage().settings.model).toBe("inception/mercury-edit")
|
||||
})
|
||||
|
||||
it("persists supported model updates", async () => {
|
||||
const post = vi.fn()
|
||||
const { routeAutocompleteMessage } = await import("../settings")
|
||||
|
||||
await routeAutocompleteMessage(
|
||||
{ type: "updateAutocompleteSetting", key: "model", value: "inception/mercury-edit" },
|
||||
post,
|
||||
)
|
||||
|
||||
expect(update).toHaveBeenCalledWith("model", "inception/mercury-edit", 1)
|
||||
expect(post).toHaveBeenCalledWith(expect.objectContaining({ type: "autocompleteSettingsLoaded" }))
|
||||
})
|
||||
|
||||
it("rejects unsupported model updates", async () => {
|
||||
const post = vi.fn()
|
||||
const { routeAutocompleteMessage } = await import("../settings")
|
||||
|
||||
await routeAutocompleteMessage({ type: "updateAutocompleteSetting", key: "model", value: "other/model" }, post)
|
||||
|
||||
expect(update).not.toHaveBeenCalled()
|
||||
expect(post).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("rejects non-boolean toggle updates", async () => {
|
||||
const post = vi.fn()
|
||||
const { routeAutocompleteMessage } = await import("../settings")
|
||||
|
||||
await routeAutocompleteMessage({ type: "updateAutocompleteSetting", key: "enableAutoTrigger", value: "true" }, post)
|
||||
|
||||
expect(update).not.toHaveBeenCalled()
|
||||
expect(post).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
+3
@@ -7,6 +7,7 @@ import { postprocessAutocompleteSuggestion } from "../classic-auto-complete/usel
|
||||
import { VisibleCodeTracker } from "../context/VisibleCodeTracker"
|
||||
import { FileIgnoreController } from "../shims/FileIgnoreController"
|
||||
import type { KiloConnectionService } from "../../cli-backend"
|
||||
import { DEFAULT_AUTOCOMPLETE_MODEL } from "../../../shared/autocomplete-models"
|
||||
import { finalizeChatSuggestion, buildChatPrefix } from "./chat-autocomplete-utils"
|
||||
|
||||
interface ChatCompletionRequestMessage {
|
||||
@@ -75,6 +76,8 @@ export class ChatTextAreaAutocomplete {
|
||||
}
|
||||
|
||||
async getCompletion(userText: string, visibleCodeContext?: VisibleCodeContext): Promise<{ suggestion: string }> {
|
||||
const cfg = vscode.workspace.getConfiguration("kilo-code.new.autocomplete")
|
||||
this.model.setModel(cfg.get<string>("model") ?? DEFAULT_AUTOCOMPLETE_MODEL.id)
|
||||
const startTime = Date.now()
|
||||
|
||||
// Build context for telemetry
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as vscode from "vscode"
|
||||
import { AUTOCOMPLETE_MODELS, DEFAULT_AUTOCOMPLETE_MODEL } from "../../shared/autocomplete-models"
|
||||
|
||||
const keys = new Set(["enableAutoTrigger", "enableSmartInlineTaskKeybinding", "enableChatAutocomplete"])
|
||||
const keys = new Set(["enableAutoTrigger", "enableSmartInlineTaskKeybinding", "enableChatAutocomplete", "model"])
|
||||
|
||||
type Message = {
|
||||
type: string
|
||||
@@ -34,6 +35,7 @@ export function buildAutocompleteSettingsMessage() {
|
||||
enableAutoTrigger: config.get<boolean>("enableAutoTrigger", true),
|
||||
enableSmartInlineTaskKeybinding: config.get<boolean>("enableSmartInlineTaskKeybinding", false),
|
||||
enableChatAutocomplete: config.get<boolean>("enableChatAutocomplete", false),
|
||||
model: config.get<string>("model", DEFAULT_AUTOCOMPLETE_MODEL.id),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -50,6 +52,7 @@ export function watchAutocompleteConfig(post: Post): vscode.Disposable {
|
||||
async function update(key: unknown, value: unknown) {
|
||||
if (typeof key !== "string") return false
|
||||
if (!keys.has(key)) return false
|
||||
if (!valid(key, value)) return false
|
||||
|
||||
await vscode.workspace
|
||||
.getConfiguration("kilo-code.new.autocomplete")
|
||||
@@ -57,3 +60,12 @@ async function update(key: unknown, value: unknown) {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function valid(key: string, value: unknown) {
|
||||
if (key === "model") {
|
||||
if (typeof value !== "string") return false
|
||||
return AUTOCOMPLETE_MODELS.some((m) => m.id === value)
|
||||
}
|
||||
|
||||
return typeof value === "boolean"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Single source of truth for autocomplete FIM model definitions.
|
||||
*
|
||||
* Shared between extension code (src/) and webview code (webview-ui/).
|
||||
* When adding a new model, update ONLY this file and package.json's
|
||||
* `kilo-code.new.autocomplete.model` enum.
|
||||
*/
|
||||
|
||||
export interface AutocompleteModelDef {
|
||||
/** Full model ID sent to the gateway, e.g. "mistralai/codestral-2508" */
|
||||
readonly id: string
|
||||
/** Human-readable label shown in the settings dropdown */
|
||||
readonly label: string
|
||||
/** Provider display name for status bar / telemetry */
|
||||
readonly provider: string
|
||||
/** FIM request temperature */
|
||||
readonly temperature: number
|
||||
}
|
||||
|
||||
const models: AutocompleteModelDef[] = [
|
||||
{
|
||||
id: "mistralai/codestral-2508",
|
||||
label: "Codestral (Mistral AI)",
|
||||
provider: "Mistral AI",
|
||||
temperature: 0.2,
|
||||
},
|
||||
{
|
||||
id: "inception/mercury-edit",
|
||||
label: "Mercury Edit (Inception)",
|
||||
provider: "Inception",
|
||||
temperature: 0,
|
||||
},
|
||||
]
|
||||
|
||||
export const AUTOCOMPLETE_MODELS: readonly AutocompleteModelDef[] = models
|
||||
|
||||
export const DEFAULT_AUTOCOMPLETE_MODEL: AutocompleteModelDef = models[0]!
|
||||
|
||||
export function getAutocompleteModel(id: string): AutocompleteModelDef {
|
||||
for (const m of models) {
|
||||
if (m.id === id) return m
|
||||
}
|
||||
return DEFAULT_AUTOCOMPLETE_MODEL
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { readFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { AUTOCOMPLETE_MODELS, DEFAULT_AUTOCOMPLETE_MODEL } from "../../src/shared/autocomplete-models"
|
||||
|
||||
describe("autocomplete model enum ↔ AUTOCOMPLETE_MODELS sync", () => {
|
||||
const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf8"))
|
||||
const prop = pkg.contributes.configuration.properties["kilo-code.new.autocomplete.model"]
|
||||
|
||||
it("package.json enum matches AUTOCOMPLETE_MODELS ids", () => {
|
||||
const ids = AUTOCOMPLETE_MODELS.map((m) => m.id)
|
||||
expect(prop.enum).toEqual(ids)
|
||||
})
|
||||
|
||||
it("package.json enumDescriptions has one entry per model", () => {
|
||||
expect(prop.enumDescriptions).toHaveLength(AUTOCOMPLETE_MODELS.length)
|
||||
})
|
||||
|
||||
it("package.json default matches DEFAULT_AUTOCOMPLETE_MODEL", () => {
|
||||
expect(prop.default).toBe(DEFAULT_AUTOCOMPLETE_MODEL.id)
|
||||
})
|
||||
})
|
||||
@@ -155,4 +155,53 @@ describe("ConfigState", () => {
|
||||
expect(s.config.snapshot).toBe(true)
|
||||
expect(s.dirty).toBe(false)
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Issue #9527: clearing an agent model override must unset it, not repopulate
|
||||
// -------------------------------------------------------------------------
|
||||
describe("clearing an agent model override (issue #9527)", () => {
|
||||
it("keeps null in the draft so the backend receives a delete sentinel", () => {
|
||||
const s = new ConfigState()
|
||||
s.handleConfigLoaded({ agent: { explore: { model: "anthropic/claude-sonnet-4-20250514" } } })
|
||||
|
||||
// User clears the Model Override field. ModeEditView now sends `null`
|
||||
// instead of `undefined` (the fix). null is the delete sentinel that
|
||||
// patchJsonc maps to jsonc-parser's remove operation.
|
||||
s.updateConfig({ agent: { explore: { model: null } } })
|
||||
|
||||
// Optimistic UI: stripNulls removes the key so the field renders empty.
|
||||
expect(s.config.agent?.explore?.model).toBeUndefined()
|
||||
expect(s.dirty).toBe(true)
|
||||
|
||||
// Draft must retain the null so it survives JSON.stringify on the wire
|
||||
// and reaches patchJsonc as an explicit delete.
|
||||
expect(s.draft.agent?.explore?.model).toBeNull()
|
||||
expect(JSON.parse(JSON.stringify(s.draft))).toEqual({
|
||||
agent: { explore: { model: null } },
|
||||
})
|
||||
})
|
||||
|
||||
it("undefined (the old buggy behavior) is dropped by JSON.stringify", () => {
|
||||
// Reproduction of the pre-fix bug: sending `undefined` results in an
|
||||
// empty patch on the wire, so the backend never deletes the override
|
||||
// and the next configUpdated pushes the stale model back into the UI.
|
||||
const draft = { agent: { explore: { model: undefined } } }
|
||||
expect(JSON.parse(JSON.stringify(draft))).toEqual({ agent: { explore: {} } })
|
||||
})
|
||||
|
||||
it("confirms the save and drops the draft once the backend acks", () => {
|
||||
const s = new ConfigState()
|
||||
s.handleConfigLoaded({ agent: { explore: { model: "anthropic/claude-sonnet-4-20250514" } } })
|
||||
s.updateConfig({ agent: { explore: { model: null } } })
|
||||
s.saveConfig()
|
||||
|
||||
// Backend removed the override and pushes the stripped config back.
|
||||
s.handleConfigUpdated({ agent: { explore: {} } })
|
||||
|
||||
expect(s.config.agent?.explore?.model).toBeUndefined()
|
||||
expect(s.dirty).toBe(false)
|
||||
expect(s.saving).toBe(false)
|
||||
expect(Object.keys(s.draft).length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,6 +28,18 @@ function args(form: FormState) {
|
||||
}
|
||||
|
||||
describe("validateCustomProvider – variant name validation", () => {
|
||||
it("allows reconnecting a disabled provider id", () => {
|
||||
const form = base()
|
||||
const out = validateCustomProvider({
|
||||
...args(form),
|
||||
disabledProviders: ["my-provider"],
|
||||
existingProviderIDs: new Set(["my-provider"]),
|
||||
})
|
||||
|
||||
expect(out.result?.providerID).toBe("my-provider")
|
||||
expect(out.errors.providerID).toBeUndefined()
|
||||
})
|
||||
|
||||
it("allows submit when reasoning is enabled with no variants", () => {
|
||||
const form = base()
|
||||
form.models[0].reasoning = true
|
||||
|
||||
@@ -2,6 +2,9 @@ import { describe, it, expect } from "bun:test"
|
||||
import { readFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
|
||||
const APP_FILE = join(__dirname, "..", "..", "webview-ui", "src", "App.tsx")
|
||||
const src = readFileSync(APP_FILE, "utf8")
|
||||
|
||||
/**
|
||||
* Static guard against the perf regression fixed in this PR.
|
||||
*
|
||||
@@ -28,9 +31,6 @@ import { join } from "node:path"
|
||||
* `tests/webview-reactivity/databridge-reactivity.test.ts`.
|
||||
*/
|
||||
describe("DataBridge shape (perf regression guard)", () => {
|
||||
const path = join(__dirname, "..", "..", "webview-ui", "src", "App.tsx")
|
||||
const src = readFileSync(path, "utf8")
|
||||
|
||||
it("DataBridge exists in App.tsx", () => {
|
||||
expect(src).toMatch(/export const DataBridge/)
|
||||
})
|
||||
@@ -59,3 +59,16 @@ describe("DataBridge shape (perf regression guard)", () => {
|
||||
expect(block).toMatch(/get\s+part\s*\(\s*\)\s*\{/)
|
||||
})
|
||||
})
|
||||
|
||||
describe("DataBridge openDiff wiring (regression guard)", () => {
|
||||
const openDiffBlock = () => {
|
||||
const match = src.match(/const\s+openDiff\s*=\s*\(diff:\s*\{[\s\S]*?\n\s*\}\n\n\s*const\s+openUrl/)
|
||||
expect(match).toBeTruthy()
|
||||
return match![0]
|
||||
}
|
||||
|
||||
it("wires openDiff to the openDiffVirtual webview message", () => {
|
||||
expect(openDiffBlock()).toMatch(/postMessage\(\{\s*type:\s*["']openDiffVirtual["']\s*,\s*diff\s*\}\)/)
|
||||
expect(src).toContain("onOpenDiff={openDiff}")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ const MONOREPO_ROOT = path.resolve(import.meta.dir, "../../../..")
|
||||
const KILO_UI_DIR = path.join(MONOREPO_ROOT, "packages/kilo-ui")
|
||||
const DATA_CONTEXT_FILE = path.join(MONOREPO_ROOT, "packages/ui/src/context/data.tsx")
|
||||
const MESSAGE_PART_FILE = path.join(MONOREPO_ROOT, "packages/ui/src/components/message-part.tsx")
|
||||
const KILO_MESSAGE_PART_FILE = path.join(MONOREPO_ROOT, "packages/kilo-ui/src/components/message-part.tsx")
|
||||
|
||||
function check(code: string): { ok: boolean; output: string } {
|
||||
const result = Bun.spawnSync(["bun", "--conditions=browser", "-e", code], {
|
||||
@@ -118,6 +119,27 @@ describe("DataProvider contract (runtime)", () => {
|
||||
expect(src).toContain("OpenFileFn")
|
||||
expect(src).toMatch(/openFile:\s*props\.onOpenFile/)
|
||||
})
|
||||
|
||||
it("DataProvider accepts onOpenDiff prop and exports OpenDiffFn (source)", () => {
|
||||
// onOpenDiff and OpenDiffFn are `kilocode_change` additions — TypeScript types
|
||||
// erased at runtime, so we verify via source analysis
|
||||
const src = fs.readFileSync(DATA_CONTEXT_FILE, "utf-8")
|
||||
expect(src).toContain("onOpenDiff")
|
||||
expect(src).toContain("OpenDiffFn")
|
||||
expect(src).toMatch(/openDiff:\s*props\.onOpenDiff/)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edit tool diff-first click contract (source)", () => {
|
||||
const src = fs.readFileSync(KILO_MESSAGE_PART_FILE, "utf-8")
|
||||
|
||||
const editBlockMatch = src.match(/ToolRegistry\.register\(\{\s*name:\s*"edit"[\s\S]*?(?=ToolRegistry\.register\(|$)/)
|
||||
const editBlock = editBlockMatch?.[0] ?? ""
|
||||
|
||||
it("edit tool derives before/after content from filediff or input", () => {
|
||||
expect(editBlock).toMatch(/filediff\?\.before\s*\?\?.*oldString/)
|
||||
expect(editBlock).toMatch(/filediff\?\.after\s*\?\?.*newString/)
|
||||
})
|
||||
})
|
||||
|
||||
describe("BasicTool export contract (runtime)", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { fetchProviderData, saveCustomProvider } from "../../src/provider-actions"
|
||||
import { disconnectProvider, fetchProviderData, saveCustomProvider } from "../../src/provider-actions"
|
||||
|
||||
type ExistingGlobal = { disabled_providers?: string[]; provider?: Record<string, unknown> }
|
||||
|
||||
@@ -26,6 +26,24 @@ function createCtx(existing: ExistingGlobal = { disabled_providers: [] }) {
|
||||
return { data: true }
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
list: async () => ({
|
||||
data: {
|
||||
all: [
|
||||
{
|
||||
id: "openai",
|
||||
name: "OpenAI",
|
||||
source: "custom",
|
||||
env: [],
|
||||
models: {},
|
||||
},
|
||||
],
|
||||
connected: ["openai"],
|
||||
default: {},
|
||||
},
|
||||
}),
|
||||
auth: async () => ({ data: {} }),
|
||||
},
|
||||
global: {
|
||||
config: {
|
||||
get: async () => ({ data: existing }),
|
||||
@@ -64,6 +82,26 @@ function createProvider() {
|
||||
}
|
||||
}
|
||||
|
||||
describe("disconnectProvider", () => {
|
||||
it("keeps configured provider enabled after disconnecting oauth override", async () => {
|
||||
const existing = {
|
||||
disabled_providers: ["openai", "groq"],
|
||||
provider: {
|
||||
openai: {
|
||||
options: { apiKey: "sk-test" },
|
||||
},
|
||||
},
|
||||
}
|
||||
const { ctx, calls, setCachedConfig } = createCtx(existing)
|
||||
|
||||
await disconnectProvider(ctx, "req", "openai", null, setCachedConfig)
|
||||
|
||||
expect(calls.remove).toEqual([{ providerID: "openai" }])
|
||||
expect(calls.config).toEqual([{ config: { disabled_providers: ["groq"] } }])
|
||||
expect(calls.refresh).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("saveCustomProvider", () => {
|
||||
it("preserves auth when the api key field is unchanged", async () => {
|
||||
const { ctx, calls, setCachedConfig } = createCtx()
|
||||
@@ -191,6 +229,50 @@ describe("saveCustomProvider", () => {
|
||||
.models
|
||||
expect(Object.values(models).every((v) => v !== null)).toBe(true)
|
||||
})
|
||||
|
||||
it("removes saved custom providers from disabled_providers when reconnecting", async () => {
|
||||
const { ctx, calls, setCachedConfig } = createCtx({ disabled_providers: ["myprovider", "openai"] })
|
||||
|
||||
await saveCustomProvider(ctx, "req", "myprovider", createProvider(), undefined, false, null, setCachedConfig)
|
||||
|
||||
expect(calls.config).toHaveLength(1)
|
||||
expect(calls.config[0].config.disabled_providers).toEqual(["openai"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("disconnectProvider", () => {
|
||||
it("adds configured providers to disabled_providers without deleting their config", async () => {
|
||||
const existing = {
|
||||
disabled_providers: ["openai"],
|
||||
provider: {
|
||||
myprovider: createProvider(),
|
||||
},
|
||||
}
|
||||
const { ctx, calls, setCachedConfig } = createCtx(existing)
|
||||
|
||||
await disconnectProvider(ctx, "req", "myprovider", null, setCachedConfig)
|
||||
|
||||
expect(calls.config).toHaveLength(1)
|
||||
expect(calls.config[0].config).toEqual({ disabled_providers: ["openai", "myprovider"] })
|
||||
expect(calls.remove).toEqual([{ providerID: "myprovider" }])
|
||||
expect(calls.refresh).toBe(1)
|
||||
expect(calls.posts).toContainEqual({ type: "providerDisconnected", requestId: "req", providerID: "myprovider" })
|
||||
})
|
||||
|
||||
it("does not duplicate configured providers already disabled", async () => {
|
||||
const existing = {
|
||||
disabled_providers: ["myprovider"],
|
||||
provider: {
|
||||
myprovider: createProvider(),
|
||||
},
|
||||
}
|
||||
const { ctx, calls, setCachedConfig } = createCtx(existing)
|
||||
|
||||
await disconnectProvider(ctx, "req", "myprovider", null, setCachedConfig)
|
||||
|
||||
expect(calls.config).toHaveLength(0)
|
||||
expect(calls.refresh).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetchProviderData", () => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
|
||||
import { visibleConnectedIds } from "../../webview-ui/src/components/settings/provider-visibility"
|
||||
import {
|
||||
disabledProviderOptions,
|
||||
providersWithKiloFallback,
|
||||
visibleConnectedIds,
|
||||
} from "../../webview-ui/src/components/settings/provider-visibility"
|
||||
|
||||
describe("visibleConnectedIds", () => {
|
||||
it("hides Kilo from the connected list when auth is missing", () => {
|
||||
@@ -21,3 +25,55 @@ describe("visibleConnectedIds", () => {
|
||||
expect(ids).toEqual(["anthropic"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("disabledProviderOptions", () => {
|
||||
it("includes Kilo and excludes already disabled providers", () => {
|
||||
const options = disabledProviderOptions(
|
||||
{
|
||||
kilo: { id: "kilo", name: "Kilo Gateway", env: [], models: {} },
|
||||
openai: { id: "openai", name: "OpenAI", env: [], models: {} },
|
||||
anthropic: { id: "anthropic", name: "Anthropic", env: [], models: {} },
|
||||
},
|
||||
["openai"],
|
||||
)
|
||||
|
||||
expect(options).toEqual([
|
||||
{ value: "anthropic", label: "Anthropic" },
|
||||
{ value: "kilo", label: "Kilo Gateway" },
|
||||
])
|
||||
})
|
||||
|
||||
it("sorts options by provider name", () => {
|
||||
const options = disabledProviderOptions(
|
||||
{
|
||||
zed: { id: "zed", name: "Zed", env: [], models: {} },
|
||||
alpha: { id: "alpha", name: "Alpha", env: [], models: {} },
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
expect(options).toEqual([
|
||||
{ value: "alpha", label: "Alpha" },
|
||||
{ value: "zed", label: "Zed" },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("providersWithKiloFallback", () => {
|
||||
it("adds Kilo when backend providers omit it", () => {
|
||||
const providers = providersWithKiloFallback({
|
||||
anthropic: { id: "anthropic", name: "Anthropic", env: [], models: {} },
|
||||
})
|
||||
|
||||
expect(providers.kilo?.name).toBe("Kilo Gateway")
|
||||
expect(providers.anthropic?.name).toBe("Anthropic")
|
||||
})
|
||||
|
||||
it("keeps the backend Kilo provider when present", () => {
|
||||
const providers = providersWithKiloFallback({
|
||||
kilo: { id: "kilo", name: "Custom Kilo Name", env: [], models: {} },
|
||||
})
|
||||
|
||||
expect(providers.kilo?.name).toBe("Custom Kilo Name")
|
||||
})
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user