Merge remote-tracking branch 'origin/main' into feat/cli-local-run

This commit is contained in:
Alex Alecu
2026-04-27 13:03:16 +03:00
89 changed files with 2963 additions and 175 deletions
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Fix two Agent Manager tab-bar regressions. New session and terminal tabs now consistently open at the right end of the tab bar instead of sometimes slipping in front of existing tabs. Dragging a terminal tab now shows the same floating label preview as dragging a session tab.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Prefer ChatGPT OAuth credentials over inherited OpenAI environment variables and make ChatGPT sign-in easier to find.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Support attaching Git changes from prompt mentions in the VS Code extension.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Add Settings header buttons to open the project and global Kilo config files directly in VS Code.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Prompt before agents access files outside the active directory when a workspace boundary resolves to a filesystem root.
+10
View File
@@ -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
@@ -181,6 +182,15 @@ Changeset descriptions appear directly in release notes and are read by end user
PR descriptions should be 2-3 lines covering **what** changed and **why**. Focus on intent and context a reviewer can't get from the diff — skip file-by-file inventories, test result summaries, and anything obvious from the code itself.
## GitHub Issues
- When creating a GitHub issue for the VS Code extension or JetBrains plugin, use the repo's existing issue templates in `.github/ISSUE_TEMPLATE/`. Pick the matching template (`Bug report`, `Feature Request`, or `Question`) instead of opening a blank issue.
- Do not add platform-specific title prefixes such as `[JetBrains]`, `[Jetbrains]`, `[JB]`, `[VS Code]`, `[VSCode]`, or similar. Use a plain, descriptive title.
- Always add VS Code extension issues to the GitHub project `VS Code Extension`: https://github.com/orgs/Kilo-Org/projects/25
- Always add JetBrains plugin issues to the GitHub project `Jetbrains Plugin`: https://github.com/orgs/Kilo-Org/projects/39
- When using `gh`, prefer `gh issue create --template "..." --project "..."` with the matching project title.
- If project assignment fails because `gh` is missing the required scope, run `gh auth refresh -s project` and retry.
## Fork Merge Process
Kilo CLI is a fork of [opencode](https://github.com/anomalyco/opencode).
+261
View File
@@ -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.
+25 -21
View File
@@ -32,7 +32,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {
"@kilocode/kilo-i18n": "workspace:*",
"@kilocode/kilo-ui": "workspace:*",
@@ -88,7 +88,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -121,7 +121,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -172,7 +172,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -201,7 +201,7 @@
},
"packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway",
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/anthropic": "3.0.71",
@@ -210,7 +210,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 +237,7 @@
},
"packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n",
"version": "7.2.22",
"version": "7.2.25",
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
@@ -250,7 +250,7 @@
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {
"@kilocode/kilo-gateway": "workspace:*",
"@opentelemetry/api": "1.9.0",
@@ -270,7 +270,7 @@
},
"packages/kilo-ui": {
"name": "@kilocode/kilo-ui",
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {
"@kobalte/core": "0.13.11",
"@opencode-ai/shared": "workspace:*",
@@ -305,7 +305,7 @@
},
"packages/kilo-vscode": {
"name": "kilo-code",
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-i18n": "workspace:*",
@@ -365,7 +365,7 @@
},
"packages/opencode": {
"name": "@kilocode/cli",
"version": "7.2.22",
"version": "7.2.25",
"bin": {
"kilo": "./bin/kilo",
"kilocode": "./bin/kilo",
@@ -400,7 +400,7 @@
"@effect/platform-node": "catalog:",
"@gitlab/gitlab-ai-provider": "3.6.0",
"@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/node-server": "1.19.11",
"@hono/node-server": "1.19.13",
"@hono/node-ws": "1.3.0",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
@@ -416,7 +416,7 @@
"@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",
@@ -520,7 +520,7 @@
},
"packages/plugin": {
"name": "@kilocode/plugin",
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"effect": "catalog:",
@@ -545,7 +545,7 @@
},
"packages/script": {
"name": "@opencode-ai/script",
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {
"semver": "^7.6.3",
},
@@ -556,7 +556,7 @@
},
"packages/sdk/js": {
"name": "@kilocode/sdk",
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -571,7 +571,7 @@
},
"packages/shared": {
"name": "@opencode-ai/shared",
"version": "7.2.22",
"version": "7.2.25",
"bin": {
"opencode": "./bin/opencode",
},
@@ -595,7 +595,7 @@
},
"packages/storybook": {
"name": "@opencode-ai/storybook",
"version": "7.2.22",
"version": "7.2.25",
"devDependencies": {
"@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:",
@@ -618,7 +618,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
@@ -1230,7 +1230,7 @@
"@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="],
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
"@hono/node-server": ["@hono/node-server@1.19.13", "", { "peerDependencies": { "hono": "^4" } }, "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ=="],
"@hono/node-ws": ["@hono/node-ws@1.3.0", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.19.2", "hono": "^4.6.0" } }, "sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q=="],
@@ -1576,7 +1576,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=="],
@@ -5098,6 +5098,8 @@
"@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
"@modelcontextprotocol/sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
"@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
@@ -5284,6 +5286,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=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-gWGYlzSlOJ9ADgz3R+cqavNejh+qC89MgEz1L9fYAI4=",
"aarch64-linux": "sha256-lTqPejyafdbe+A+c9fKMcB2nCC3/m1opKgL8kuia2Vk=",
"aarch64-darwin": "sha256-c/QIB0g9VTm8SyJLla1hUx/QEHiZvQbQm7CBskOCozU=",
"x86_64-darwin": "sha256-qDwWCCSKl9OLkTo+HH9TdTOhGACCeFNgGX7rCV9mNUo="
"x86_64-linux": "sha256-uDu9FY2G8j6AzpXAQ3WoWLJ9uyh5R7xqcAVzij6dpdU=",
"aarch64-linux": "sha256-GtVSse+wWwmFA9e3XkAp+1spPH2u4C2jqJ9HUKQRJ44=",
"aarch64-darwin": "sha256-UrZIY7v59gB0k0+/CIXA/NFrR3FSE+5Lcsz9qZffsqM=",
"x86_64-darwin": "sha256-/thTDanC0f1rczDj7RbgeFyaRfuhMdbRXrgfH6KRejs="
}
}
+1 -1
View File
@@ -146,6 +146,6 @@
"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.22",
"version": "7.2.25",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "7.2.22",
"version": "7.2.25",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
"version": "7.2.22",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "7.2.22",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"scripts": {
+49 -49
View File
@@ -2299,6 +2299,53 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "kilo-desktop"
version = "0.0.0"
dependencies = [
"chrono",
"comrak",
"dirs",
"futures",
"gtk",
"listeners",
"objc2 0.6.3",
"objc2-web-kit",
"process-wrap",
"reqwest 0.12.24",
"semver",
"serde",
"serde_json",
"specta",
"specta-typescript",
"tauri",
"tauri-build 2.5.2",
"tauri-plugin-clipboard-manager",
"tauri-plugin-decorum",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-http",
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-os",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-store",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tauri-specta",
"tokio",
"tokio-stream",
"tracing",
"tracing-appender",
"tracing-subscriber",
"uuid",
"webkit2gtk",
"windows-core 0.62.2",
"windows-sys 0.61.2",
]
[[package]]
name = "kuchikiki"
version = "0.8.8-speedreader"
@@ -3093,53 +3140,6 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "kilo-desktop"
version = "0.0.0"
dependencies = [
"chrono",
"comrak",
"dirs",
"futures",
"gtk",
"listeners",
"objc2 0.6.3",
"objc2-web-kit",
"process-wrap",
"reqwest 0.12.24",
"semver",
"serde",
"serde_json",
"specta",
"specta-typescript",
"tauri",
"tauri-build 2.5.2",
"tauri-plugin-clipboard-manager",
"tauri-plugin-decorum",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-http",
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-os",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-store",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tauri-specta",
"tokio",
"tokio-stream",
"tracing",
"tracing-appender",
"tracing-subscriber",
"uuid",
"webkit2gtk",
"windows-core 0.62.2",
"windows-sys 0.61.2",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -4163,9 +4163,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.103.8"
version = "0.103.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
dependencies = [
"ring",
"rustls-pki-types",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "1.14.17"
version = "7.2.25"
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/v1.14.17/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.14.17/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.14.17/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/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/v1.14.17/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/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/v1.14.17/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+5
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "7.2.22",
"version": "7.2.25",
"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)
@@ -76,6 +76,61 @@ Check [our provider docs](/docs/ai-providers) for specific context limits on eac
**Recover from context limit errors:** If you hit the `input length and max tokens exceed context limit` error, you can recover by deleting a message, rolling back to a previous checkpoint, or switching over to a model with a long context window like Gemini for a message.
{% /callout %}
## Models During Delegation
When an agent delegates work to a subagent (via the `task` tool), the subagent **inherits the parent agent's model** by default. You can override this per subagent in your config:
{% tabs %}
{% tab label="CLI" %}
```json
{
"agent": {
"explore": {
"model": "anthropic/claude-haiku-4-20250514"
}
}
}
```
This sets the `explore` subagent to always use Haiku regardless of the parent's model. Any subagent without a `model` override uses whatever model the invoking agent is running.
{% /tab %}
{% tab label="VSCode" %}
Subagents inherit the model currently active in the primary agent session — the model shown in the selector at the bottom of the chat. To bypass inheritance and pin a specific model for a subagent:
- **Via Settings** — open **Settings → Models → Model per Mode**, find the subagent, and pick its model.
- **Via config file** — edit `kilo.jsonc`:
```json
{
"agent": {
"explore": {
"model": "anthropic/claude-haiku-4-5"
}
}
}
```
The Settings UI writes the same `agent.<name>.model` entry, so either method produces the same override. Subagents without an explicit model continue to inherit whatever the invoking agent is running.
{% /tab %}
{% tab label="VSCode (Legacy)" %}
In the legacy extension, each mode has **Sticky Models** — switching from one mode to another (e.g., Code → Architect) uses whatever model you last selected for that mode, not the model from the mode you came from. This means you can assign different models to different modes:
- **Architect:** a reasoning-heavy model (Gemini Pro, Claude Opus)
- **Code:** a fast coding model (Claude Sonnet, GPT-4.1)
- **Debug:** a cost-efficient model (Gemini Flash, DeepSeek)
The model selection is remembered per mode across sessions.
{% /tab %}
{% /tabs %}
For details on configuring subagent models, see [Custom Subagents](/docs/customize/custom-subagents).
## Stay Current
The AI model space moves fast. Bookmark [kilo.ai/models](https://kilo.ai/models) and check back when you're evaluating options. What's best today might not be best next month — and that's actually exciting.
@@ -232,11 +232,27 @@ If you have existing `.kilocodemodes` or `custom_modes.yaml` files from the VSCo
Default legacy mode slugs (`code`, `build`, `architect`, `ask`, `debug`, `orchestrator`) are skipped during migration since they map to built-in agents (`build``code`, `architect``plan`).
### Legacy File Locations
The current VSCode extension reads the legacy `custom_modes.yaml` file from its own global storage directory. Helpful for inspecting or fixing the file before the one-time migration runs:
| OS | Path |
| ------- | ----------------------------------------------------------------------------------------------------- |
| macOS | `~/Library/Application Support/Code/User/globalStorage/kilocode.kilo-code/settings/custom_modes.yaml` |
| Linux | `~/.config/Code/User/globalStorage/kilocode.kilo-code/settings/custom_modes.yaml` |
| Windows | `%APPDATA%\Code\User\globalStorage\kilocode.kilo-code\settings\custom_modes.yaml` |
Project-level `.kilocodemodes` and workspace-scoped files are handled by the CLI backend that the extension delegates to — see the [CLI tab](#cli) for the full load-order table. After the extension migrates on startup, the legacy file is no longer consulted; remove new modes through the extension UI instead of editing `custom_modes.yaml` directly.
{% /tab %}
{% tab label="CLI" %}
In the CLI, custom behavioral profiles are called **agents** instead of modes. Agents are defined as Markdown files with YAML frontmatter or as entries in the `agent` key of your config file.
{% callout type="warning" %}
**Legacy `custom_modes.yaml` is not loaded from `~/.config/kilo/`.** If you're migrating from the legacy VSCode extension, global custom modes are read from `~/.kilocode/cli/global/settings/custom_modes.yaml` (not from the CLI's XDG config directory). The recommended approach is to convert legacy modes to agent `.md` files and place them in `~/.config/kilo/agent/` instead — see [Markdown files](#3-markdown-files-with-yaml-frontmatter) and [Migration](#migration-from-vscode-extension-modes) below.
{% /callout %}
## What's Included in a Custom Agent?
| Property | Description |
@@ -452,6 +468,21 @@ If you have existing `.kilocodemodes` or `custom_modes.yaml` files from the VSCo
Default legacy mode slugs (`code`, `build`, `architect`, `ask`, `debug`, `orchestrator`) are skipped during migration since they map to built-in agents (`build``code`, `architect``plan`).
### Legacy File Lookup Paths
The CLI reads legacy mode files from the following locations (in load order). When the same slug appears in multiple sources, the **last loaded source wins**:
| Load Order | Path | Format | Scope |
|------------|------|--------|-------|
| 1 | VSCode extension global storage `/settings/custom_modes.yaml` | YAML | Global |
| 2 | `~/.kilocode/cli/global/settings/custom_modes.yaml` | YAML | Global |
| 3 | `~/.kilocodemodes` | YAML | Global |
| 4 | `<project>/.kilocodemodes` | YAML | Project (wins on conflict) |
{% callout type="info" %}
`~/.config/kilo/` is the XDG config directory for the new agent format — legacy `custom_modes.yaml` placed there will **not** be loaded. Use `~/.config/kilo/agent/*.md` or `~/.config/kilo/kilo.jsonc` for new agent definitions instead.
{% /callout %}
{% /tab %}
{% tab label="VSCode (Legacy)" %}
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4524d1fe8b44cf2cce15bdf3ad100572b57fa479fefe456f1c141ebca8d5187d
size 28078
oid sha256:16e602e6a870e4b66a466a0895f92e9df117f6137809c79ed797f24d2b6a1e88
size 31028
+1
View File
@@ -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 -->
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "7.2.22",
"version": "7.2.25",
"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",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "7.2.22",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
"version": "7.2.22",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
"version": "7.2.22",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"exports": {
+10
View File
@@ -1,5 +1,15 @@
# kilo-code
## 7.2.25
## 7.2.23
### Patch Changes
- [#9423](https://github.com/Kilo-Org/kilocode/pull/9423) [`a87a461`](https://github.com/Kilo-Org/kilocode/commit/a87a461e971864377d2139c35e778cc98d5cca19) - Fix two Agent Manager tab-bar regressions. New session and terminal tabs now consistently open at the right end of the tab bar instead of sometimes slipping in front of existing tabs. Dragging a terminal tab now shows the same floating label preview as dragging a session tab.
- [#9026](https://github.com/Kilo-Org/kilocode/pull/9026) [`61516b4`](https://github.com/Kilo-Org/kilocode/commit/61516b4d3f5a87f623a34b9112b8e1ff3725f93c) - Support attaching Git changes from prompt mentions in the VS Code extension.
## 7.2.22
### Patch Changes
+10 -1
View File
@@ -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.22",
"version": "7.2.25",
"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": [
{
+4
View File
@@ -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))
}
}
+21 -1
View File
@@ -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,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()
@@ -14,6 +14,7 @@ import { createProviderAction } from "../../utils/provider-action"
interface ProviderConnectDialogProps {
providerID: string
oauthOnly?: boolean
}
interface ViewState {
@@ -49,7 +50,10 @@ const ProviderConnectDialog: Component<ProviderConnectDialogProps> = (props) =>
const item = createMemo(() => provider.providers()[props.providerID])
const name = () => item()?.name ?? props.providerID
const methods = createMemo<ProviderAuthMethod[]>(() => {
return provider.authMethods()[props.providerID] ?? fallbackMethods(language.t("provider.connect.method.apiKey"))
const list =
provider.authMethods()[props.providerID] ?? fallbackMethods(language.t("provider.connect.method.apiKey"))
if (props.oauthOnly) return list.filter((item) => item.type === "oauth")
return list
})
const method = createMemo(() => {
const index = state.methodIndex
@@ -75,6 +75,7 @@ const ProvidersTab: Component = () => {
if (cfg?.npm === "@ai-sdk/openai-compatible") return language.t("settings.providers.tag.custom")
return language.t("settings.providers.tag.config")
}
if (item.id === "openai" && current === "custom") return language.t("settings.providers.tag.chatgpt")
if (current === "custom") return language.t("settings.providers.tag.custom")
return language.t("settings.providers.tag.other")
}
@@ -121,6 +122,16 @@ const ProvidersTab: Component = () => {
dialog.show(() => <ProviderConnectDialog providerID={item.id} />)
}
function connectChatGPT(item: Provider) {
dialog.show(() => <ProviderConnectDialog providerID={item.id} oauthOnly />)
}
function chatgpt(item: Provider) {
if (item.id !== "openai") return false
if (source(item) === "custom") return false
return (provider.authMethods()[item.id] ?? []).some((method) => method.type === "oauth")
}
return (
<div>
{/* Kilo Gateway — always at the top, not editable */}
@@ -200,9 +211,8 @@ const ProvidersTab: Component = () => {
</span>
<Tag>{sourceTag(item)}</Tag>
</div>
<Show
when={canDisconnect(item)}
fallback={
<div style={{ display: "flex", "align-items": "center", gap: "4px" }}>
<Show when={!canDisconnect(item)}>
<span
style={{
"font-size": "14px",
@@ -212,9 +222,13 @@ const ProvidersTab: Component = () => {
>
{language.t("settings.providers.connected.environmentDescription")}
</span>
}
>
<div style={{ display: "flex", gap: "4px" }}>
</Show>
<Show when={chatgpt(item)}>
<Button size="large" variant="ghost" onClick={() => connectChatGPT(item)}>
{language.t("settings.providers.action.signInChatGPT")}
</Button>
</Show>
<Show when={canDisconnect(item)}>
<Show when={isCustom(item)}>
<Button size="large" variant="ghost" onClick={() => editProvider(item)}>
{language.t("provider.custom.edit.title")}
@@ -223,8 +237,8 @@ const ProvidersTab: Component = () => {
<Button size="large" variant="ghost" onClick={() => disconnect(item.id, item.name)}>
{language.t("common.disconnect")}
</Button>
</div>
</Show>
</Show>
</div>
</div>
)}
</For>
@@ -59,6 +59,37 @@ const Settings: Component<SettingsProps> = (props) => {
})
}
const open = (scope: "local" | "global") => {
const label =
scope === "global" ? language.t("settings.config.scope.global") : language.t("settings.config.scope.local")
vscode.postMessage({
type: "openConfigFile",
scope,
labels: {
scope: label,
statusLoaded: language.t("settings.config.status.loaded"),
statusLoadedLegacy: language.t("settings.config.status.loadedLegacy"),
statusNotLoaded: language.t("settings.config.status.notLoaded"),
statusCreate: language.t("settings.config.status.create"),
title: language.t("settings.config.title", { scope: label }),
placeholder: language.t("settings.config.placeholder"),
noWorkspace: language.t("settings.config.noWorkspace"),
openFailed: language.t("settings.config.openFailed", { scope: label, message: "{{message}}" }),
sourceXdg: language.t("settings.config.source.xdg"),
sourceHomeKilo: language.t("settings.config.source.homeKilo"),
sourceHomeKilocode: language.t("settings.config.source.homeKilocode"),
sourceHomeOpencode: language.t("settings.config.source.homeOpencode"),
sourceEnvFile: language.t("settings.config.source.envFile"),
sourceEnvDir: language.t("settings.config.source.envDir"),
sourceEnvContent: language.t("settings.config.source.envContent"),
sourceProjectKilo: language.t("settings.config.source.projectKilo"),
sourceProjectRoot: language.t("settings.config.source.projectRoot"),
sourceProjectKilocode: language.t("settings.config.source.projectKilocode"),
sourceProjectOpencode: language.t("settings.config.source.projectOpencode"),
},
})
}
// Sync when the parent changes the tab prop (e.g. via navigate message)
createEffect(
on(
@@ -84,10 +115,19 @@ const Settings: Component<SettingsProps> = (props) => {
"border-bottom": "1px solid var(--border-weak-base)",
display: "flex",
"align-items": "center",
"flex-wrap": "wrap",
gap: "8px",
}}
>
<h2 style={{ "font-size": "16px", "font-weight": "600", margin: 0 }}>{language.t("sidebar.settings")}</h2>
<h2 style={{ "font-size": "16px", "font-weight": "600", margin: 0, flex: 1 }}>
{language.t("sidebar.settings")}
</h2>
<Button variant="secondary" size="small" icon="edit" onClick={() => open("local")}>
{language.t("settings.openLocalConfig")}
</Button>
<Button variant="secondary" size="small" icon="edit" onClick={() => open("global")}>
{language.t("settings.openGlobalConfig")}
</Button>
</div>
{/* Settings tabs */}
+25
View File
@@ -734,10 +734,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "البيئة",
"settings.providers.tag.config": "التكوين",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "مخصص",
"settings.providers.tag.other": "أخرى",
"settings.providers.tag.customProvider": "مزود مخصص",
"settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك",
"settings.providers.action.signInChatGPT": "تسجيل الدخول باستخدام ChatGPT",
"settings.providers.custom.description": "أضف مزوداً متوافقاً مع OpenAI عبر عنوان URL الأساسي.",
"settings.providers.modeModels": "نموذج لكل وضع",
"settings.providers.custom.note": "أضف موفرًا متوافقًا مع OpenAI عبر عنوان URL الأساسي.",
@@ -820,6 +822,29 @@ export const dict = {
"provider.custom.error.baseURL.format": "يجب أن يبدأ بـ http:// أو https://",
"provider.custom.error.required": "مطلوب",
"provider.custom.error.duplicate": "مكرر",
"settings.openLocalConfig": "تكوين محلي",
"settings.openGlobalConfig": "تكوين عام",
"settings.config.scope.local": "محلي",
"settings.config.scope.global": "عالمي",
"settings.config.status.loaded": "محمل",
"settings.config.status.loadedLegacy": "محمل تكوين قديم",
"settings.config.status.notLoaded": "غير محمل",
"settings.config.status.create": "غير موجود - قم بإنشاء هذا الملف",
"settings.config.title": "فتح ملف تكوين Kilo {{scope}}",
"settings.config.placeholder": "يتم دمج ملفات التكوين بالترتيب؛ الملفات المحددة كمحملة تؤثر حاليًا على الإعدادات.",
"settings.config.noWorkspace": "افتح مجلد مساحة عمل لتحرير ملف تكوين Kilo المحلي.",
"settings.config.openFailed": "فشل فتح ملف تكوين Kilo {{scope}}: {{message}}",
"settings.config.source.xdg": "تكوين XDG العالمي",
"settings.config.source.homeKilo": "تكوين .kilo في Home",
"settings.config.source.homeKilocode": "تكوين .kilocode في Home",
"settings.config.source.homeOpencode": "تكوين .opencode في Home",
"settings.config.source.envFile": "ملف بيئة KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "تكوين البيئة المضمن",
"settings.config.source.projectKilo": "تكوين .kilo للمشروع",
"settings.config.source.projectRoot": "تكوين جذر المشروع",
"settings.config.source.projectKilocode": "تكوين .kilocode القديم",
"settings.config.source.projectOpencode": "تكوين .opencode القديم",
"settings.models.title": "النماذج",
"settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.",
"settings.agents.title": "الوكلاء",
+27
View File
@@ -741,10 +741,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Ambiente",
"settings.providers.tag.config": "Configuração",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Personalizado",
"settings.providers.tag.other": "Outro",
"settings.providers.tag.customProvider": "Provedor personalizado",
"settings.providers.connected.environmentDescription": "Conectado a partir das suas variáveis de ambiente",
"settings.providers.action.signInChatGPT": "Entrar com ChatGPT",
"settings.providers.custom.description": "Adicione um provedor compatível com OpenAI pela URL base.",
"settings.providers.modeModels": "Modelo por Modo",
"settings.providers.custom.note": "Adicione um provedor compatível com OpenAI por URL base.",
@@ -828,6 +830,31 @@ export const dict = {
"provider.custom.error.baseURL.format": "Deve começar com http:// ou https://",
"provider.custom.error.required": "Obrigatório",
"provider.custom.error.duplicate": "Duplicado",
"settings.openLocalConfig": "Config Local",
"settings.openGlobalConfig": "Config Global",
"settings.config.scope.local": "Local",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "carregado",
"settings.config.status.loadedLegacy": "configuração legada carregada",
"settings.config.status.notLoaded": "não carregado",
"settings.config.status.create": "não encontrado - criar este arquivo",
"settings.config.title": "Abrir arquivo de configuração {{scope}} do Kilo",
"settings.config.placeholder":
"Os arquivos de configuração são mesclados em ordem; os arquivos marcados como carregados afetam atualmente as configurações.",
"settings.config.noWorkspace":
"Abra uma pasta de espaço de trabalho para editar o arquivo de configuração local do Kilo.",
"settings.config.openFailed": "Falha ao abrir o arquivo de configuração {{scope}} do Kilo: {{message}}",
"settings.config.source.xdg": "Configuração global do XDG",
"settings.config.source.homeKilo": "Configuração .kilo da Home",
"settings.config.source.homeKilocode": "Configuração .kilocode da Home",
"settings.config.source.homeOpencode": "Configuração .opencode da Home",
"settings.config.source.envFile": "Arquivo de ambiente KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Configuração de ambiente embutida",
"settings.config.source.projectKilo": "Configuração .kilo do projeto",
"settings.config.source.projectRoot": "Configuração raiz do projeto",
"settings.config.source.projectKilocode": "Configuração .kilocode legada",
"settings.config.source.projectOpencode": "Configuração .opencode legada",
"settings.models.title": "Modelos",
"settings.models.description": "Configurações de modelos estarão disponíveis aqui.",
"settings.agents.title": "Agentes",
+26
View File
@@ -745,10 +745,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Okruženje",
"settings.providers.tag.config": "Konfiguracija",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Prilagođeno",
"settings.providers.tag.other": "Ostalo",
"settings.providers.tag.customProvider": "Prilagođeni provajder",
"settings.providers.connected.environmentDescription": "Povezano iz vaših varijabli okruženja",
"settings.providers.action.signInChatGPT": "Prijavi se putem ChatGPT",
"settings.providers.custom.description": "Dodaj OpenAI-kompatibilan provajder putem osnovnog URL-a.",
"settings.providers.modeModels": "Model po režimu",
"settings.providers.custom.note": "Dodajte provajdera kompatibilnog s OpenAI putem osnovnog URL-a.",
@@ -834,6 +836,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "Mora početi sa http:// ili https://",
"provider.custom.error.required": "Obavezno",
"provider.custom.error.duplicate": "Duplikat",
"settings.openLocalConfig": "Lokalna konfig.",
"settings.openGlobalConfig": "Globalna konfig.",
"settings.config.scope.local": "Lokalno",
"settings.config.scope.global": "Globalno",
"settings.config.status.loaded": "učitano",
"settings.config.status.loadedLegacy": "učitana zastarjela konfiguracija",
"settings.config.status.notLoaded": "nije učitano",
"settings.config.status.create": "nije pronađeno - kreiraj ovu datoteku",
"settings.config.title": "Otvori {{scope}} Kilo konfiguracijsku datoteku",
"settings.config.placeholder":
"Konfiguracijske datoteke se spajaju po redu; datoteke označene kao učitane trenutno utiču na postavke.",
"settings.config.noWorkspace": "Otvorite fasciklu radnog prostora da uredite lokalnu Kilo konfiguracijsku datoteku.",
"settings.config.openFailed": "Nije uspjelo otvaranje {{scope}} Kilo konfiguracijske datoteke: {{message}}",
"settings.config.source.xdg": "XDG globalna konfiguracija",
"settings.config.source.homeKilo": "Home .kilo konfiguracija",
"settings.config.source.homeKilocode": "Home .kilocode konfiguracija",
"settings.config.source.homeOpencode": "Home .opencode konfiguracija",
"settings.config.source.envFile": "KILO_CONFIG datoteka okruženja",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Ugrađena konfiguracija okruženja",
"settings.config.source.projectKilo": "Projektna .kilo konfiguracija",
"settings.config.source.projectRoot": "Konfiguracija korijena projekta",
"settings.config.source.projectKilocode": "Zastarjela .kilocode konfiguracija",
"settings.config.source.projectOpencode": "Zastarjela .opencode konfiguracija",
"settings.models.title": "Modeli",
"settings.models.description": "Postavke modela će se ovdje moći podešavati.",
"settings.agents.title": "Agenti",
+26
View File
@@ -740,10 +740,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Miljø",
"settings.providers.tag.config": "Konfiguration",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Brugerdefineret",
"settings.providers.tag.other": "Andet",
"settings.providers.tag.customProvider": "Brugerdefineret udbyder",
"settings.providers.connected.environmentDescription": "Forbundet fra dine miljøvariabler",
"settings.providers.action.signInChatGPT": "Log ind med ChatGPT",
"settings.providers.custom.description": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.",
"settings.providers.modeModels": "Model pr. tilstand",
"settings.providers.custom.note": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.",
@@ -827,6 +829,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "Skal starte med http:// eller https://",
"provider.custom.error.required": "Påkrævet",
"provider.custom.error.duplicate": "Duplikat",
"settings.openLocalConfig": "Lokal konfig",
"settings.openGlobalConfig": "Global konfig",
"settings.config.scope.local": "Lokal",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "indlæst",
"settings.config.status.loadedLegacy": "indlæst forældet konfiguration",
"settings.config.status.notLoaded": "ikke indlæst",
"settings.config.status.create": "ikke fundet - opret denne fil",
"settings.config.title": "Åbn {{scope}} Kilo konfigurationsfil",
"settings.config.placeholder":
"Konfigurationsfiler flettes i rækkefølge; filer markeret som indlæst påvirker i øjeblikket indstillingerne.",
"settings.config.noWorkspace": "Åbn en arbejdsområdemappe for at redigere den lokale Kilo konfigurationsfil.",
"settings.config.openFailed": "Kunne ikke åbne {{scope}} Kilo konfigurationsfil: {{message}}",
"settings.config.source.xdg": "XDG global konfiguration",
"settings.config.source.homeKilo": "Home .kilo konfiguration",
"settings.config.source.homeKilocode": "Home .kilocode konfiguration",
"settings.config.source.homeOpencode": "Home .opencode konfiguration",
"settings.config.source.envFile": "KILO_CONFIG miljøfil",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Indbygget miljøkonfiguration",
"settings.config.source.projectKilo": "Projekt .kilo konfiguration",
"settings.config.source.projectRoot": "Projekt rodkonfiguration",
"settings.config.source.projectKilocode": "Forældet .kilocode konfiguration",
"settings.config.source.projectOpencode": "Forældet .opencode konfiguration",
"settings.models.title": "Modeller",
"settings.models.description": "Modelindstillinger vil kunne konfigureres her.",
"settings.agents.title": "Agenter",
+27
View File
@@ -750,10 +750,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Umgebung",
"settings.providers.tag.config": "Konfiguration",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Benutzerdefiniert",
"settings.providers.tag.other": "Andere",
"settings.providers.tag.customProvider": "Benutzerdefinierter Anbieter",
"settings.providers.connected.environmentDescription": "Verbunden über Ihre Umgebungsvariablen",
"settings.providers.action.signInChatGPT": "Mit ChatGPT anmelden",
"settings.providers.custom.description": "Fügen Sie einen OpenAI-kompatiblen Anbieter über die Basis-URL hinzu.",
"settings.providers.modeModels": "Modell pro Modus",
"settings.providers.custom.note": "Fügen Sie einen OpenAI-kompatiblen Anbieter per Basis-URL hinzu.",
@@ -839,6 +841,31 @@ export const dict = {
"provider.custom.error.baseURL.format": "Muss mit http:// oder https:// beginnen",
"provider.custom.error.required": "Erforderlich",
"provider.custom.error.duplicate": "Duplikat",
"settings.openLocalConfig": "Lokale Config",
"settings.openGlobalConfig": "Globale Config",
"settings.config.scope.local": "Lokal",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "geladen",
"settings.config.status.loadedLegacy": "veraltete Konfiguration geladen",
"settings.config.status.notLoaded": "nicht geladen",
"settings.config.status.create": "nicht gefunden - diese Datei erstellen",
"settings.config.title": "{{scope}} Kilo-Konfigurationsdatei öffnen",
"settings.config.placeholder":
"Konfigurationsdateien werden der Reihe nach zusammengeführt; als geladen markierte Dateien wirken sich aktuell auf die Einstellungen aus.",
"settings.config.noWorkspace":
"Öffnen Sie einen Arbeitsbereichsordner, um die lokale Kilo-Konfigurationsdatei zu bearbeiten.",
"settings.config.openFailed": "Fehler beim Öffnen der {{scope}} Kilo-Konfigurationsdatei: {{message}}",
"settings.config.source.xdg": "Globale XDG-Konfiguration",
"settings.config.source.homeKilo": "Home .kilo-Konfiguration",
"settings.config.source.homeKilocode": "Home .kilocode-Konfiguration",
"settings.config.source.homeOpencode": "Home .opencode-Konfiguration",
"settings.config.source.envFile": "KILO_CONFIG Umgebungsdatei",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Inline-Umgebungskonfiguration",
"settings.config.source.projectKilo": "Projekt .kilo-Konfiguration",
"settings.config.source.projectRoot": "Projektstamm-Konfiguration",
"settings.config.source.projectKilocode": "Veraltete .kilocode-Konfiguration",
"settings.config.source.projectOpencode": "Veraltete .opencode-Konfiguration",
"settings.models.title": "Modelle",
"settings.models.description": "Modelleinstellungen können hier konfiguriert werden.",
"settings.agents.title": "Agenten",
+26 -1
View File
@@ -106,7 +106,7 @@ export const dict = {
"dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more",
"dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max",
"dialog.provider.copilot.note": "Claude models for coding assistance",
"dialog.provider.openai.note": "GPT models for fast, capable general AI tasks",
"dialog.provider.openai.note": "GPT and Codex models with API key or ChatGPT login",
"dialog.provider.google.note": "Gemini models for fast, structured responses",
"dialog.provider.openrouter.note": "Access all supported models from one provider",
"dialog.provider.vercel.note": "Unified access to AI models with smart routing",
@@ -744,10 +744,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Environment",
"settings.providers.tag.config": "Config",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Custom",
"settings.providers.tag.customProvider": "Custom provider",
"settings.providers.tag.other": "Other",
"settings.providers.connected.environmentDescription": "Connected from your environment variables",
"settings.providers.action.signInChatGPT": "Sign in with ChatGPT",
"settings.providers.custom.description": "Add an OpenAI-compatible provider by base URL.",
"provider.custom.title": "Custom provider",
@@ -828,6 +830,29 @@ export const dict = {
"provider.custom.error.baseURL.format": "Must start with http:// or https://",
"provider.custom.error.required": "Required",
"provider.custom.error.duplicate": "Duplicate",
"settings.openLocalConfig": "Local Config",
"settings.openGlobalConfig": "Global Config",
"settings.config.scope.local": "Local",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "loaded",
"settings.config.status.loadedLegacy": "loaded legacy config",
"settings.config.status.notLoaded": "not loaded",
"settings.config.status.create": "not found - create this file",
"settings.config.title": "Open {{scope}} Kilo config file",
"settings.config.placeholder": "Config files are merged in order; files marked loaded currently affect settings.",
"settings.config.noWorkspace": "Open a workspace folder to edit the local Kilo config file.",
"settings.config.openFailed": "Failed to open {{scope}} Kilo config file: {{message}}",
"settings.config.source.xdg": "XDG global config",
"settings.config.source.homeKilo": "Home .kilo config",
"settings.config.source.homeKilocode": "Home .kilocode config",
"settings.config.source.homeOpencode": "Home .opencode config",
"settings.config.source.envFile": "KILO_CONFIG environment file",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Inline environment config",
"settings.config.source.projectKilo": "Project .kilo config",
"settings.config.source.projectRoot": "Project root config",
"settings.config.source.projectKilocode": "Legacy .kilocode config",
"settings.config.source.projectOpencode": "Legacy .opencode config",
"settings.models.title": "Models",
"settings.models.description": "Model settings will be configurable here.",
"settings.agents.title": "Agents",
+27
View File
@@ -746,10 +746,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Entorno",
"settings.providers.tag.config": "Configuración",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Personalizado",
"settings.providers.tag.other": "Otro",
"settings.providers.tag.customProvider": "Proveedor personalizado",
"settings.providers.connected.environmentDescription": "Conectado desde tus variables de entorno",
"settings.providers.action.signInChatGPT": "Iniciar sesión con ChatGPT",
"settings.providers.custom.description": "Añade un proveedor compatible con OpenAI por URL base.",
"settings.providers.modeModels": "Modelo por modo",
"settings.providers.custom.note": "Agrega un proveedor compatible con OpenAI mediante URL base.",
@@ -834,6 +836,31 @@ export const dict = {
"provider.custom.error.baseURL.format": "Debe empezar con http:// o https://",
"provider.custom.error.required": "Obligatorio",
"provider.custom.error.duplicate": "Duplicado",
"settings.openLocalConfig": "Config local",
"settings.openGlobalConfig": "Config global",
"settings.config.scope.local": "Local",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "cargado",
"settings.config.status.loadedLegacy": "configuración heredada cargada",
"settings.config.status.notLoaded": "no cargado",
"settings.config.status.create": "no encontrado - crear este archivo",
"settings.config.title": "Abrir archivo de configuración Kilo {{scope}}",
"settings.config.placeholder":
"Los archivos de configuración se combinan en orden; los archivos marcados como cargados afectan actualmente a los ajustes.",
"settings.config.noWorkspace":
"Abre una carpeta de espacio de trabajo para editar el archivo de configuración Kilo local.",
"settings.config.openFailed": "Error al abrir el archivo de configuración Kilo {{scope}}: {{message}}",
"settings.config.source.xdg": "Configuración global XDG",
"settings.config.source.homeKilo": "Configuración .kilo de Home",
"settings.config.source.homeKilocode": "Configuración .kilocode de Home",
"settings.config.source.homeOpencode": "Configuración .opencode de Home",
"settings.config.source.envFile": "Archivo de entorno KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Configuración de entorno en línea",
"settings.config.source.projectKilo": "Configuración .kilo del proyecto",
"settings.config.source.projectRoot": "Configuración raíz del proyecto",
"settings.config.source.projectKilocode": "Configuración heredada .kilocode",
"settings.config.source.projectOpencode": "Configuración heredada .opencode",
"settings.models.title": "Modelos",
"settings.models.description": "La configuración de modelos estará disponible aquí.",
"settings.agents.title": "Agentes",
+27
View File
@@ -750,10 +750,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Environnement",
"settings.providers.tag.config": "Configuration",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Personnalisé",
"settings.providers.tag.other": "Autre",
"settings.providers.tag.customProvider": "Fournisseur personnalisé",
"settings.providers.connected.environmentDescription": "Connecté depuis vos variables d'environnement",
"settings.providers.action.signInChatGPT": "Se connecter avec ChatGPT",
"settings.providers.custom.description": "Ajoutez un fournisseur compatible OpenAI par URL de base.",
"settings.providers.modeModels": "Modèle par mode",
"settings.providers.custom.note": "Ajoutez un fournisseur compatible OpenAI par URL de base.",
@@ -839,6 +841,31 @@ export const dict = {
"provider.custom.error.baseURL.format": "Doit commencer par http:// ou https://",
"provider.custom.error.required": "Requis",
"provider.custom.error.duplicate": "Doublon",
"settings.openLocalConfig": "Config locale",
"settings.openGlobalConfig": "Config globale",
"settings.config.scope.local": "Local",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "chargé",
"settings.config.status.loadedLegacy": "configuration obsolète chargée",
"settings.config.status.notLoaded": "non chargé",
"settings.config.status.create": "introuvable - créer ce fichier",
"settings.config.title": "Ouvrir le fichier de configuration Kilo {{scope}}",
"settings.config.placeholder":
"Les fichiers de configuration sont fusionnés dans l'ordre ; les fichiers marqués comme chargés affectent actuellement les paramètres.",
"settings.config.noWorkspace":
"Ouvrez un dossier d'espace de travail pour modifier le fichier de configuration Kilo local.",
"settings.config.openFailed": "Échec de l'ouverture du fichier de configuration Kilo {{scope}} : {{message}}",
"settings.config.source.xdg": "Configuration globale XDG",
"settings.config.source.homeKilo": "Configuration .kilo de Home",
"settings.config.source.homeKilocode": "Configuration .kilocode de Home",
"settings.config.source.homeOpencode": "Configuration .opencode de Home",
"settings.config.source.envFile": "Fichier d'environnement KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Configuration d'environnement en ligne",
"settings.config.source.projectKilo": "Configuration .kilo du projet",
"settings.config.source.projectRoot": "Configuration racine du projet",
"settings.config.source.projectKilocode": "Configuration obsolète .kilocode",
"settings.config.source.projectOpencode": "Configuration obsolète .opencode",
"settings.models.title": "Modèles",
"settings.models.description": "Les paramètres des modèles seront configurables ici.",
"settings.agents.title": "Agents",
+26
View File
@@ -739,10 +739,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "環境",
"settings.providers.tag.config": "設定",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "カスタム",
"settings.providers.tag.other": "その他",
"settings.providers.tag.customProvider": "カスタムプロバイダー",
"settings.providers.connected.environmentDescription": "環境変数から接続されています",
"settings.providers.action.signInChatGPT": "ChatGPT でサインイン",
"settings.providers.custom.description": "ベースURLでOpenAI互換プロバイダーを追加します。",
"settings.providers.modeModels": "モードごとのモデル",
"settings.providers.custom.note": "Base URL で OpenAI 互換プロバイダーを追加します。",
@@ -825,6 +827,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "http:// または https:// で始まる必要があります",
"provider.custom.error.required": "必須",
"provider.custom.error.duplicate": "重複",
"settings.openLocalConfig": "ローカル設定",
"settings.openGlobalConfig": "グローバル設定",
"settings.config.scope.local": "ローカル",
"settings.config.scope.global": "グローバル",
"settings.config.status.loaded": "読み込み済み",
"settings.config.status.loadedLegacy": "旧構成を読み込み済み",
"settings.config.status.notLoaded": "読み込まれていません",
"settings.config.status.create": "見つかりません - このファイルを作成する",
"settings.config.title": "{{scope}}のKilo構成ファイルを開く",
"settings.config.placeholder":
"構成ファイルは順番にマージされます。読み込み済みとしてマークされているファイルが現在設定に影響しています。",
"settings.config.noWorkspace": "ローカルのKilo構成ファイルを編集するには、ワークスペースフォルダーを開いてください。",
"settings.config.openFailed": "{{scope}}のKilo構成ファイルを開けませんでした: {{message}}",
"settings.config.source.xdg": "XDGグローバル構成",
"settings.config.source.homeKilo": "Homeの.kilo構成",
"settings.config.source.homeKilocode": "Homeの.kilocode構成",
"settings.config.source.homeOpencode": "Homeの.opencode構成",
"settings.config.source.envFile": "KILO_CONFIG環境ファイル",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "インライン環境構成",
"settings.config.source.projectKilo": "プロジェクトの.kilo構成",
"settings.config.source.projectRoot": "プロジェクトルート構成",
"settings.config.source.projectKilocode": "旧.kilocode構成",
"settings.config.source.projectOpencode": "旧.opencode構成",
"settings.models.title": "モデル",
"settings.models.description": "モデル設定はここで構成できます。",
"settings.agents.title": "エージェント",
+26
View File
@@ -739,10 +739,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "환경",
"settings.providers.tag.config": "구성",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "사용자 지정",
"settings.providers.tag.other": "기타",
"settings.providers.tag.customProvider": "사용자 정의 공급자",
"settings.providers.connected.environmentDescription": "환경 변수에서 연결됨",
"settings.providers.action.signInChatGPT": "ChatGPT로 로그인",
"settings.providers.custom.description": "기본 URL로 OpenAI 호환 공급자를 추가합니다.",
"settings.providers.modeModels": "모드별 모델",
"settings.providers.custom.note": "Base URL로 OpenAI 호환 공급자를 추가합니다.",
@@ -825,6 +827,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "http:// 또는 https://로 시작해야 합니다",
"provider.custom.error.required": "필수",
"provider.custom.error.duplicate": "중복",
"settings.openLocalConfig": "로컬 설정",
"settings.openGlobalConfig": "전역 설정",
"settings.config.scope.local": "로컬",
"settings.config.scope.global": "글로벌",
"settings.config.status.loaded": "로드됨",
"settings.config.status.loadedLegacy": "레거시 구성 로드됨",
"settings.config.status.notLoaded": "로드되지 않음",
"settings.config.status.create": "찾을 수 없음 - 이 파일 만들기",
"settings.config.title": "{{scope}} Kilo 구성 파일 열기",
"settings.config.placeholder":
"구성 파일은 순서대로 병합됩니다. 로드됨으로 표시된 파일이 현재 설정에 영향을 미칩니다.",
"settings.config.noWorkspace": "로컬 Kilo 구성 파일을 편집하려면 작업 영역 폴더를 엽니다.",
"settings.config.openFailed": "{{scope}} Kilo 구성 파일을 열지 못했습니다: {{message}}",
"settings.config.source.xdg": "XDG 글로벌 구성",
"settings.config.source.homeKilo": "Home .kilo 구성",
"settings.config.source.homeKilocode": "Home .kilocode 구성",
"settings.config.source.homeOpencode": "Home .opencode 구성",
"settings.config.source.envFile": "KILO_CONFIG 환경 파일",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "인라인 환경 구성",
"settings.config.source.projectKilo": "프로젝트 .kilo 구성",
"settings.config.source.projectRoot": "프로젝트 루트 구성",
"settings.config.source.projectKilocode": "레거시 .kilocode 구성",
"settings.config.source.projectOpencode": "레거시 .opencode 구성",
"settings.models.title": "모델",
"settings.models.description": "모델 설정은 여기서 구성할 수 있습니다.",
"settings.agents.title": "에이전트",
+26
View File
@@ -743,10 +743,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Omgeving",
"settings.providers.tag.config": "Config",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Aangepast",
"settings.providers.tag.customProvider": "Aangepaste provider",
"settings.providers.tag.other": "Overige",
"settings.providers.connected.environmentDescription": "Gekoppeld via je omgevingsvariabelen",
"settings.providers.action.signInChatGPT": "Inloggen met ChatGPT",
"settings.providers.custom.description": "Voeg een OpenAI-compatibele provider toe via basis-URL.",
"provider.custom.title": "Aangepaste provider",
@@ -828,6 +830,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "Moet beginnen met http:// of https://",
"provider.custom.error.required": "Vereist",
"provider.custom.error.duplicate": "Duplicaat",
"settings.openLocalConfig": "Lokale config",
"settings.openGlobalConfig": "Globale config",
"settings.config.scope.local": "Lokaal",
"settings.config.scope.global": "Globaal",
"settings.config.status.loaded": "geladen",
"settings.config.status.loadedLegacy": "verouderde configuratie geladen",
"settings.config.status.notLoaded": "niet geladen",
"settings.config.status.create": "niet gevonden - maak dit bestand",
"settings.config.title": "Open {{scope}} Kilo-configuratiebestand",
"settings.config.placeholder":
"Configuratiebestanden worden op volgorde samengevoegd; bestanden gemarkeerd als geladen hebben momenteel invloed op de instellingen.",
"settings.config.noWorkspace": "Open een werkruimtemap om het lokale Kilo-configuratiebestand te bewerken.",
"settings.config.openFailed": "Kan {{scope}} Kilo-configuratiebestand niet openen: {{message}}",
"settings.config.source.xdg": "XDG globale configuratie",
"settings.config.source.homeKilo": "Home .kilo-configuratie",
"settings.config.source.homeKilocode": "Home .kilocode-configuratie",
"settings.config.source.homeOpencode": "Home .opencode-configuratie",
"settings.config.source.envFile": "KILO_CONFIG omgevingsbestand",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Inline omgevingsconfiguratie",
"settings.config.source.projectKilo": "Project .kilo-configuratie",
"settings.config.source.projectRoot": "Project root configuratie",
"settings.config.source.projectKilocode": "Verouderde .kilocode-configuratie",
"settings.config.source.projectOpencode": "Verouderde .opencode-configuratie",
"settings.models.title": "Modellen",
"settings.models.description": "Model-instellingen zullen hier configureerbaar zijn.",
"settings.agents.title": "Agenten",
+26
View File
@@ -744,10 +744,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Miljø",
"settings.providers.tag.config": "Konfigurasjon",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Tilpasset",
"settings.providers.tag.other": "Annet",
"settings.providers.tag.customProvider": "Egendefinert leverandør",
"settings.providers.connected.environmentDescription": "Koblet til fra dine miljøvariabler",
"settings.providers.action.signInChatGPT": "Logg inn med ChatGPT",
"settings.providers.custom.description": "Legg til en OpenAI-kompatibel leverandør via basis-URL.",
"settings.providers.modeModels": "Modell per modus",
"settings.providers.custom.note": "Legg til en OpenAI-kompatibel leverandør via basis-URL.",
@@ -831,6 +833,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "Må starte med http:// eller https://",
"provider.custom.error.required": "Påkrevd",
"provider.custom.error.duplicate": "Duplikat",
"settings.openLocalConfig": "Lokal konfig",
"settings.openGlobalConfig": "Global konfig",
"settings.config.scope.local": "Lokal",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "lastet",
"settings.config.status.loadedLegacy": "lastet inn eldre konfigurasjon",
"settings.config.status.notLoaded": "ikke lastet",
"settings.config.status.create": "ikke funnet - opprett denne filen",
"settings.config.title": "Åpne {{scope}} Kilo-konfigurasjonsfil",
"settings.config.placeholder":
"Konfigurasjonsfiler slås sammen i rekkefølge; filer merket som lastet påvirker for øyeblikket innstillingene.",
"settings.config.noWorkspace": "Åpne en arbeidsområdemappe for å redigere den lokale Kilo-konfigurasjonsfilen.",
"settings.config.openFailed": "Klarte ikke å åpne {{scope}} Kilo-konfigurasjonsfil: {{message}}",
"settings.config.source.xdg": "XDG global konfigurasjon",
"settings.config.source.homeKilo": "Home .kilo-konfigurasjon",
"settings.config.source.homeKilocode": "Home .kilocode-konfigurasjon",
"settings.config.source.homeOpencode": "Home .opencode-konfigurasjon",
"settings.config.source.envFile": "KILO_CONFIG miljøfil",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Innebygd miljøkonfigurasjon",
"settings.config.source.projectKilo": "Prosjekt .kilo-konfigurasjon",
"settings.config.source.projectRoot": "Prosjektets rotkonfigurasjon",
"settings.config.source.projectKilocode": "Eldre .kilocode-konfigurasjon",
"settings.config.source.projectOpencode": "Eldre .opencode-konfigurasjon",
"settings.models.title": "Modeller",
"settings.models.description": "Modellinnstillinger vil kunne konfigureres her.",
"settings.agents.title": "Agenter",
+26
View File
@@ -743,10 +743,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Środowisko",
"settings.providers.tag.config": "Konfiguracja",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Niestandardowe",
"settings.providers.tag.other": "Inne",
"settings.providers.tag.customProvider": "Niestandardowy dostawca",
"settings.providers.connected.environmentDescription": "Połączony z twoich zmiennych środowiskowych",
"settings.providers.action.signInChatGPT": "Zaloguj przez ChatGPT",
"settings.providers.custom.description": "Dodaj dostawcę kompatybilnego z OpenAI przez bazowy URL.",
"settings.providers.modeModels": "Model na tryb",
"settings.providers.custom.note": "Dodaj dostawcę kompatybilnego z OpenAI przez bazowy URL.",
@@ -832,6 +834,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "Musi zaczynać się od http:// lub https://",
"provider.custom.error.required": "Wymagane",
"provider.custom.error.duplicate": "Duplikat",
"settings.openLocalConfig": "Konfig. lokalna",
"settings.openGlobalConfig": "Konfig. globalna",
"settings.config.scope.local": "Lokalne",
"settings.config.scope.global": "Globalne",
"settings.config.status.loaded": "wczytano",
"settings.config.status.loadedLegacy": "wczytano przestarzałą konfigurację",
"settings.config.status.notLoaded": "nie wczytano",
"settings.config.status.create": "nie znaleziono - utwórz ten plik",
"settings.config.title": "Otwórz plik konfiguracyjny Kilo ({{scope}})",
"settings.config.placeholder":
"Pliki konfiguracyjne są scalane po kolei; pliki oznaczone jako wczytane mają obecnie wpływ na ustawienia.",
"settings.config.noWorkspace": "Otwórz folder obszaru roboczego, aby edytować lokalny plik konfiguracyjny Kilo.",
"settings.config.openFailed": "Nie udało się otworzyć pliku konfiguracyjnego Kilo ({{scope}}): {{message}}",
"settings.config.source.xdg": "Globalna konfiguracja XDG",
"settings.config.source.homeKilo": "Konfiguracja .kilo (Home)",
"settings.config.source.homeKilocode": "Konfiguracja .kilocode (Home)",
"settings.config.source.homeOpencode": "Konfiguracja .opencode (Home)",
"settings.config.source.envFile": "Plik środowiskowy KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Wbudowana konfiguracja środowiska",
"settings.config.source.projectKilo": "Konfiguracja .kilo projektu",
"settings.config.source.projectRoot": "Konfiguracja główna projektu",
"settings.config.source.projectKilocode": "Przestarzała konfiguracja .kilocode",
"settings.config.source.projectOpencode": "Przestarzała konfiguracja .opencode",
"settings.models.title": "Modele",
"settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.",
"settings.agents.title": "Agenci",
+27
View File
@@ -745,10 +745,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Среда",
"settings.providers.tag.config": "Конфигурация",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Пользовательский",
"settings.providers.tag.other": "Другое",
"settings.providers.tag.customProvider": "Пользовательский провайдер",
"settings.providers.connected.environmentDescription": "Подключён из ваших переменных окружения",
"settings.providers.action.signInChatGPT": "Войти через ChatGPT",
"settings.providers.custom.description": "Добавьте OpenAI-совместимый провайдер по базовому URL.",
"settings.providers.modeModels": "Модель для режима",
"settings.providers.custom.note": "Добавьте OpenAI-совместимого провайдера по базовому URL.",
@@ -833,6 +835,31 @@ export const dict = {
"provider.custom.error.baseURL.format": "Должен начинаться с http:// или https://",
"provider.custom.error.required": "Обязательно",
"provider.custom.error.duplicate": "Дубликат",
"settings.openLocalConfig": "Локальный конфиг",
"settings.openGlobalConfig": "Глобальный конфиг",
"settings.config.scope.local": "Локальный",
"settings.config.scope.global": "Глобальный",
"settings.config.status.loaded": "загружено",
"settings.config.status.loadedLegacy": "загружена устаревшая конфигурация",
"settings.config.status.notLoaded": "не загружено",
"settings.config.status.create": "не найдено - создать этот файл",
"settings.config.title": "Открыть файл конфигурации Kilo ({{scope}})",
"settings.config.placeholder":
"Файлы конфигурации объединяются по порядку; файлы, отмеченные как загруженные, в данный момент влияют на настройки.",
"settings.config.noWorkspace":
"Откройте папку рабочей области для редактирования локального файла конфигурации Kilo.",
"settings.config.openFailed": "Не удалось открыть файл конфигурации Kilo ({{scope}}): {{message}}",
"settings.config.source.xdg": "Глобальная конфигурация XDG",
"settings.config.source.homeKilo": "Конфигурация .kilo (Home)",
"settings.config.source.homeKilocode": "Конфигурация .kilocode (Home)",
"settings.config.source.homeOpencode": "Конфигурация .opencode (Home)",
"settings.config.source.envFile": "Файл среды KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Встроенная конфигурация среды",
"settings.config.source.projectKilo": "Конфигурация .kilo проекта",
"settings.config.source.projectRoot": "Корневая конфигурация проекта",
"settings.config.source.projectKilocode": "Устаревшая конфигурация .kilocode",
"settings.config.source.projectOpencode": "Устаревшая конфигурация .opencode",
"settings.models.title": "Модели",
"settings.models.description": "Настройки моделей будут доступны здесь.",
"settings.agents.title": "Агенты",
+26
View File
@@ -737,10 +737,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "สภาพแวดล้อม",
"settings.providers.tag.config": "กำหนดค่า",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "กำหนดเอง",
"settings.providers.tag.other": "อื่น ๆ",
"settings.providers.tag.customProvider": "ผู้ให้บริการที่กำหนดเอง",
"settings.providers.connected.environmentDescription": "เชื่อมต่อจากตัวแปรสภาพแวดล้อมของคุณ",
"settings.providers.action.signInChatGPT": "ลงชื่อเข้าใช้ด้วย ChatGPT",
"settings.providers.custom.description": "เพิ่มผู้ให้บริการที่เข้ากันได้กับ OpenAI ด้วย URL พื้นฐาน",
"settings.providers.modeModels": "โมเดลต่อโหมด",
"settings.providers.custom.note": "เพิ่มผู้ให้บริการที่รองรับ OpenAI ด้วย Base URL",
@@ -824,6 +826,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "ต้องขึ้นต้นด้วย http:// หรือ https://",
"provider.custom.error.required": "จำเป็น",
"provider.custom.error.duplicate": "ซ้ำ",
"settings.openLocalConfig": "คอนฟิก Local",
"settings.openGlobalConfig": "คอนฟิก Global",
"settings.config.scope.local": "ภายใน",
"settings.config.scope.global": "ทั่วโลก",
"settings.config.status.loaded": "โหลดแล้ว",
"settings.config.status.loadedLegacy": "โหลดการตั้งค่าแบบเก่าแล้ว",
"settings.config.status.notLoaded": "ยังไม่ได้โหลด",
"settings.config.status.create": "ไม่พบ - สร้างไฟล์นี้",
"settings.config.title": "เปิดไฟล์การตั้งค่า Kilo ({{scope}})",
"settings.config.placeholder":
"ไฟล์การตั้งค่าจะถูกผสานตามลำดับ ไฟล์ที่ถูกทำเครื่องหมายว่าโหลดแล้วจะมีผลกับการตั้งค่าในปัจจุบัน",
"settings.config.noWorkspace": "เปิดโฟลเดอร์พื้นที่ทำงานเพื่อแก้ไขไฟล์การตั้งค่า Kilo ภายใน",
"settings.config.openFailed": "ไม่สามารถเปิดไฟล์การตั้งค่า Kilo ({{scope}}): {{message}}",
"settings.config.source.xdg": "การตั้งค่า XDG ทั่วโลก",
"settings.config.source.homeKilo": "การตั้งค่า .kilo ของ Home",
"settings.config.source.homeKilocode": "การตั้งค่า .kilocode ของ Home",
"settings.config.source.homeOpencode": "การตั้งค่า .opencode ของ Home",
"settings.config.source.envFile": "ไฟล์สภาพแวดล้อม KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "การตั้งค่าสภาพแวดล้อมแบบอินไลน์",
"settings.config.source.projectKilo": "การตั้งค่า .kilo ของโปรเจกต์",
"settings.config.source.projectRoot": "การตั้งค่ารูทของโปรเจกต์",
"settings.config.source.projectKilocode": "การตั้งค่า .kilocode แบบเก่า",
"settings.config.source.projectOpencode": "การตั้งค่า .opencode แบบเก่า",
"settings.models.title": "โมเดล",
"settings.models.description": "การตั้งค่าโมเดลจะสามารถกำหนดค่าได้ที่นี่",
"settings.agents.title": "เอเจนต์",
+26
View File
@@ -744,10 +744,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Ortam",
"settings.providers.tag.config": "Yapılandırma",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Özel",
"settings.providers.tag.customProvider": "Özel sağlayıcı",
"settings.providers.tag.other": "Diğer",
"settings.providers.connected.environmentDescription": "Ortam değişkenlerinizden bağlandı",
"settings.providers.action.signInChatGPT": "ChatGPT ile oturum aç",
"settings.providers.custom.description": "Temel URL üzerinden OpenAI uyumlu bir sağlayıcı ekleyin.",
"provider.custom.title": "Özel sağlayıcı",
@@ -830,6 +832,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "http:// veya https:// ile başlamalı",
"provider.custom.error.required": "Gerekli",
"provider.custom.error.duplicate": "Tekrar",
"settings.openLocalConfig": "Yerel Config",
"settings.openGlobalConfig": "Global Config",
"settings.config.scope.local": "Yerel",
"settings.config.scope.global": "Küresel",
"settings.config.status.loaded": "yüklendi",
"settings.config.status.loadedLegacy": "eski yapılandırma yüklendi",
"settings.config.status.notLoaded": "yüklenmedi",
"settings.config.status.create": "bulunamadı - bu dosyayı oluştur",
"settings.config.title": "{{scope}} Kilo yapılandırma dosyasını aç",
"settings.config.placeholder":
"Yapılandırma dosyaları sırayla birleştirilir; yüklendi olarak işaretlenen dosyalar şu anda ayarları etkiler.",
"settings.config.noWorkspace": "Yerel Kilo yapılandırma dosyasını düzenlemek için bir çalışma alanı klasörü açın.",
"settings.config.openFailed": "{{scope}} Kilo yapılandırma dosyası açılamadı: {{message}}",
"settings.config.source.xdg": "XDG küresel yapılandırma",
"settings.config.source.homeKilo": "Home .kilo yapılandırması",
"settings.config.source.homeKilocode": "Home .kilocode yapılandırması",
"settings.config.source.homeOpencode": "Home .opencode yapılandırması",
"settings.config.source.envFile": "KILO_CONFIG ortam dosyası",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Satır içi ortam yapılandırması",
"settings.config.source.projectKilo": "Proje .kilo yapılandırması",
"settings.config.source.projectRoot": "Proje kök yapılandırması",
"settings.config.source.projectKilocode": "Eski .kilocode yapılandırması",
"settings.config.source.projectOpencode": "Eski .opencode yapılandırması",
"settings.models.title": "Modeller",
"settings.models.description": "Model ayarları burada yapılandırılabilecek.",
"settings.agents.title": "Ajanlar",
+26
View File
@@ -745,10 +745,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Середовище",
"settings.providers.tag.config": "Конфігурація",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Власний",
"settings.providers.tag.customProvider": "Власний провайдер",
"settings.providers.tag.other": "Інші",
"settings.providers.connected.environmentDescription": "Підключено зі змінних середовища",
"settings.providers.action.signInChatGPT": "Увійти через ChatGPT",
"settings.providers.custom.description": "Додати OpenAI-сумісного провайдера через базовий URL.",
"provider.custom.title": "Власний провайдер",
@@ -830,6 +832,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "Має починатися з http:// або https://",
"provider.custom.error.required": "Обов'язкове поле",
"provider.custom.error.duplicate": "Дублікат",
"settings.openLocalConfig": "Локальний конфіг",
"settings.openGlobalConfig": "Глобальний конфіг",
"settings.config.scope.local": "Локальний",
"settings.config.scope.global": "Глобальний",
"settings.config.status.loaded": "завантажено",
"settings.config.status.loadedLegacy": "завантажено застарілу конфігурацію",
"settings.config.status.notLoaded": "не завантажено",
"settings.config.status.create": "не знайдено - створити цей файл",
"settings.config.title": "Відкрити файл конфігурації Kilo ({{scope}})",
"settings.config.placeholder":
"Файли конфігурації об'єднуються по порядку; файли, позначені як завантажені, наразі впливають на налаштування.",
"settings.config.noWorkspace": "Відкрийте папку робочої області, щоб відредагувати локальний файл конфігурації Kilo.",
"settings.config.openFailed": "Не вдалося відкрити файл конфігурації Kilo ({{scope}}): {{message}}",
"settings.config.source.xdg": "Глобальна конфігурація XDG",
"settings.config.source.homeKilo": "Конфігурація .kilo (Home)",
"settings.config.source.homeKilocode": "Конфігурація .kilocode (Home)",
"settings.config.source.homeOpencode": "Конфігурація .opencode (Home)",
"settings.config.source.envFile": "Файл середовища KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Вбудована конфігурація середовища",
"settings.config.source.projectKilo": "Конфігурація .kilo проєкту",
"settings.config.source.projectRoot": "Коренева конфігурація проєкту",
"settings.config.source.projectKilocode": "Застаріла конфігурація .kilocode",
"settings.config.source.projectOpencode": "Застаріла конфігурація .opencode",
"settings.models.title": "Моделі",
"settings.models.description": "Тут можна буде налаштовувати параметри моделей.",
"settings.agents.title": "Агенти",
+25
View File
@@ -729,10 +729,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "环境",
"settings.providers.tag.config": "配置",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "自定义",
"settings.providers.tag.other": "其他",
"settings.providers.tag.customProvider": "自定义提供商",
"settings.providers.connected.environmentDescription": "从您的环境变量连接",
"settings.providers.action.signInChatGPT": "使用 ChatGPT 登录",
"settings.providers.custom.description": "通过基础 URL 添加 OpenAI 兼容的提供商。",
"settings.providers.modeModels": "按模式选择模型",
"settings.providers.custom.note": "通过 Base URL 添加 OpenAI 兼容提供商。",
@@ -814,6 +816,29 @@ export const dict = {
"provider.custom.error.baseURL.format": "必须以 http:// 或 https:// 开头",
"provider.custom.error.required": "必填",
"provider.custom.error.duplicate": "重复",
"settings.openLocalConfig": "本地配置",
"settings.openGlobalConfig": "全局配置",
"settings.config.scope.local": "本地",
"settings.config.scope.global": "全局",
"settings.config.status.loaded": "已加载",
"settings.config.status.loadedLegacy": "已加载旧版配置",
"settings.config.status.notLoaded": "未加载",
"settings.config.status.create": "未找到 - 创建此文件",
"settings.config.title": "打开 {{scope}} Kilo 配置文件",
"settings.config.placeholder": "配置文件按顺序合并;标记为已加载的文件目前会影响设置。",
"settings.config.noWorkspace": "打开工作区文件夹以编辑本地 Kilo 配置文件。",
"settings.config.openFailed": "无法打开 {{scope}} Kilo 配置文件:{{message}}",
"settings.config.source.xdg": "XDG 全局配置",
"settings.config.source.homeKilo": "主目录 .kilo 配置",
"settings.config.source.homeKilocode": "主目录 .kilocode 配置",
"settings.config.source.homeOpencode": "主目录 .opencode 配置",
"settings.config.source.envFile": "KILO_CONFIG 环境变量文件",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "内联环境配置",
"settings.config.source.projectKilo": "项目 .kilo 配置",
"settings.config.source.projectRoot": "项目根目录配置",
"settings.config.source.projectKilocode": "旧版 .kilocode 配置",
"settings.config.source.projectOpencode": "旧版 .opencode 配置",
"settings.models.title": "模型",
"settings.models.description": "模型设置将在此处可配置。",
"settings.agents.title": "智能体",
+25
View File
@@ -731,10 +731,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "環境",
"settings.providers.tag.config": "設定",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "自訂",
"settings.providers.tag.other": "其他",
"settings.providers.tag.customProvider": "自訂提供商",
"settings.providers.connected.environmentDescription": "從您的環境變數連線",
"settings.providers.action.signInChatGPT": "使用 ChatGPT 登入",
"settings.providers.custom.description": "透過基礎 URL 新增 OpenAI 相容的提供商。",
"settings.providers.modeModels": "按模式選擇模型",
"settings.providers.custom.note": "透過 Base URL 新增 OpenAI 相容供應商。",
@@ -816,6 +818,29 @@ export const dict = {
"provider.custom.error.baseURL.format": "必須以 http:// 或 https:// 開頭",
"provider.custom.error.required": "必填",
"provider.custom.error.duplicate": "重複",
"settings.openLocalConfig": "本機設定",
"settings.openGlobalConfig": "全域設定",
"settings.config.scope.local": "本地",
"settings.config.scope.global": "全域",
"settings.config.status.loaded": "已載入",
"settings.config.status.loadedLegacy": "已載入舊版設定",
"settings.config.status.notLoaded": "未載入",
"settings.config.status.create": "找不到 - 建立此檔案",
"settings.config.title": "開啟 {{scope}} Kilo 設定檔",
"settings.config.placeholder": "設定檔會按順序合併;標記為已載入的檔案目前會影響設定。",
"settings.config.noWorkspace": "開啟工作區資料夾以編輯本地 Kilo 設定檔。",
"settings.config.openFailed": "無法開啟 {{scope}} Kilo 設定檔:{{message}}",
"settings.config.source.xdg": "XDG 全域設定",
"settings.config.source.homeKilo": "主目錄 .kilo 設定",
"settings.config.source.homeKilocode": "主目錄 .kilocode 設定",
"settings.config.source.homeOpencode": "主目錄 .opencode 設定",
"settings.config.source.envFile": "KILO_CONFIG 環境變數檔案",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "內聯環境設定",
"settings.config.source.projectKilo": "專案 .kilo 設定",
"settings.config.source.projectRoot": "專案根目錄設定",
"settings.config.source.projectKilocode": "舊版 .kilocode 設定",
"settings.config.source.projectOpencode": "舊版 .opencode 設定",
"settings.models.title": "模型",
"settings.models.description": "可在此調整模型設定。",
"settings.agents.title": "代理程式",
@@ -163,6 +163,33 @@ export interface OpenVSCodeSettingsRequest {
query: string
}
export interface OpenConfigFileRequest {
type: "openConfigFile"
scope: "local" | "global"
labels: {
scope: string
statusLoaded: string
statusLoadedLegacy: string
statusNotLoaded: string
statusCreate: string
title: string
placeholder: string
noWorkspace: string
openFailed: string
sourceXdg: string
sourceHomeKilo: string
sourceHomeKilocode: string
sourceHomeOpencode: string
sourceEnvFile: string
sourceEnvDir: string
sourceEnvContent: string
sourceProjectKilo: string
sourceProjectRoot: string
sourceProjectKilocode: string
sourceProjectOpencode: string
}
}
export interface OpenMarketplacePanelRequest {
type: "openMarketplacePanel"
}
@@ -901,6 +928,7 @@ export type WebviewMessage =
| OpenExternalRequest
| OpenSettingsPanelRequest
| OpenVSCodeSettingsRequest
| OpenConfigFileRequest
| OpenMarketplacePanelRequest
| OpenFileRequest
| CancelLoginRequest
+20
View File
@@ -1,5 +1,25 @@
# @kilocode/cli
## 7.2.25
### Patch Changes
- [#9526](https://github.com/Kilo-Org/kilocode/pull/9526) [`c8113f2`](https://github.com/Kilo-Org/kilocode/commit/c8113f27b190f5c08ce642da57d68646132e1828) - Fix multi-turn DeepSeek reasoning round-tripping on OpenRouter by bumping `@openrouter/ai-sdk-provider` to 2.8.1 in both the CLI and Kilo Gateway packages and letting the SDK handle reasoning details, plus pulling in upstream DeepSeek variant, reasoning-effort, and assistant-reasoning fixes. New DeepSeek conversations are fixed; existing sessions that already stored empty reasoning metadata may still need to be restarted.
- Updated dependencies [[`c8113f2`](https://github.com/Kilo-Org/kilocode/commit/c8113f27b190f5c08ce642da57d68646132e1828)]:
- @kilocode/kilo-gateway@7.2.25
- @kilocode/kilo-telemetry@7.2.25
## 7.2.23
### Minor Changes
- [#9418](https://github.com/Kilo-Org/kilocode/pull/9418) [`12c2d86`](https://github.com/Kilo-Org/kilocode/commit/12c2d86c84ecfce118ffb5b4db7ed4155bbca8fc) - Show the open GitHub PR for the current branch in the session sidebar.
### Patch Changes
- [#9470](https://github.com/Kilo-Org/kilocode/pull/9470) [`7fe4508`](https://github.com/Kilo-Org/kilocode/commit/7fe4508eecf7e7da8336f75c0884d1b310af6c6e) - Fix multi-turn tool calls with DeepSeek thinking mode by preserving empty `reasoning_content` in the interleaved transform.
## 7.2.22
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.2.22",
"version": "7.2.25",
"name": "@kilocode/cli",
"type": "module",
"license": "MIT",
@@ -105,7 +105,7 @@
"@effect/platform-node": "catalog:",
"@gitlab/gitlab-ai-provider": "3.6.0",
"@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/node-server": "1.19.11",
"@hono/node-server": "1.19.13",
"@hono/node-ws": "1.3.0",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
@@ -121,7 +121,7 @@
"@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",
+5 -2
View File
@@ -391,7 +391,7 @@ export const ProvidersLoginCommand = cmd({
hint: {
kilo: "recommended", // kilocode_change
opencode: "recommended",
openai: "ChatGPT Plus/Pro or API key",
openai: "ChatGPT login or API key", // kilocode_change
}[x.id],
})),
),
@@ -407,7 +407,10 @@ export const ProvidersLoginCommand = cmd({
const input = args.provider
const byID = options.find((x) => x.value === input)
const byName = options.find((x) => x.label.toLowerCase() === input.toLowerCase())
const match = byID ?? byName
// kilocode_change start - accept codex as an alias for OpenAI ChatGPT auth
const alias = input.toLowerCase() === "codex" ? options.find((x) => x.value === "openai") : undefined
const match = byID ?? byName ?? alias
// kilocode_change end
if (!match) {
prompts.log.error(`Unknown provider "${input}"`)
process.exit(1)
@@ -33,7 +33,7 @@ export function createDialogProviderOptions() {
const connected = sync.data.provider_next.connected.includes(provider.id)
return {
title: provider.name,
title: KiloProvider.PROVIDER_TITLES[provider.id] ?? provider.name, // kilocode_change
value: provider.id,
description: KiloProvider.PROVIDER_DESCRIPTIONS[provider.id], // kilocode_change
footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined,
@@ -5,6 +5,7 @@ import HomeNews from "@/kilocode/plugins/home-news"
import HomeOnboarding from "@/kilocode/plugins/home-onboarding"
import KiloHomeFooter from "@/kilocode/plugins/home-footer"
import KiloSidebarFooter from "@/kilocode/plugins/sidebar-footer"
import KiloSidebarPr from "@/kilocode/plugins/sidebar-pr"
import KiloSidebarUsage from "@/kilocode/plugins/sidebar-usage"
// kilocode_change end
import SidebarContext from "../feature-plugins/sidebar/context"
@@ -26,6 +27,7 @@ export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [
HomeOnboarding, // kilocode_change
KiloHomeFooter, // kilocode_change
KiloSidebarFooter, // kilocode_change
KiloSidebarPr, // kilocode_change
KiloSidebarUsage, // kilocode_change
HomeFooter,
HomeTips,
@@ -30,7 +30,11 @@ export const PROVIDER_PRIORITY: Record<string, number> = {
export const PROVIDER_DESCRIPTIONS: Record<string, string> = {
kilo: "(Recommended)",
anthropic: "(Claude Max or API key)",
openai: "(ChatGPT Plus/Pro or API key)",
openai: "(ChatGPT login or API key)",
}
export const PROVIDER_TITLES: Record<string, string> = {
openai: "OpenAI / Codex",
}
// ---------------------------------------------------------------------------
@@ -0,0 +1,286 @@
// kilocode_change - new file
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@kilocode/plugin/tui"
import { createMemo, createResource, Show } from "solid-js"
import { Process } from "@/util"
const id = "internal:kilo-sidebar-pr"
const GH_PROBE_TTL = 300_000
type Pr = { number: number; title: string }
type Item = Partial<Pr> & { headRefOid?: string }
type Repo = {
nameWithOwner?: unknown
parent?: { nameWithOwner?: unknown; name?: unknown; owner?: { login?: unknown } } | null
}
let ghPath: string | null | undefined
let ghProbeTime = 0
function wait(ms: number, signal: AbortSignal) {
return new Promise<void>((resolve, reject) => {
const done = () => {
clearTimeout(id)
signal.removeEventListener("abort", stop)
}
const stop = () => {
done()
reject(signal.reason)
}
const id = setTimeout(() => {
done()
resolve()
}, ms)
signal.addEventListener("abort", stop, { once: true })
})
}
async function lookup(cwd: string, branch: string): Promise<Pr | null> {
const ctrl = new AbortController()
const deadline = wait(20_000, ctrl.signal).then(() => ctrl.abort())
try {
if (!probeGh()) return null
// Try the tracking ref first (works when PR was checked out via `gh pr checkout`
// or when the branch's upstream is a fork). Fall back to an explicit branch
// lookup (works for same-repo branches pushed to origin).
const build = (b?: string) => {
const a = ["gh", "pr", "view"]
if (b) a.push(b)
a.push("--json", "number,title")
return a
}
for (const cmd of [build(), build(branch)]) {
const res = await Process.text(cmd, {
cwd,
abort: ctrl.signal,
nothrow: true,
timeout: 1_000,
})
if (res.code !== 0) continue
const text = res.text.trim()
if (!text) continue
const data = JSON.parse(text) as Partial<Pr>
if (typeof data.number === "number" && typeof data.title === "string") {
return { number: data.number, title: data.title }
}
}
const head = await lookupHead(cwd, ctrl.signal)
if (!head) return null
const local = await lookupBySha(cwd, head, undefined, ctrl.signal)
if (local) return local
const parent = await lookupParent(cwd, ctrl.signal)
if (!parent) return null
const pr = await lookupByHead(cwd, branch, head, parent, ctrl.signal)
if (pr) return pr
return await lookupBySha(cwd, head, parent, ctrl.signal)
} finally {
ctrl.abort()
await deadline.catch(() => undefined)
}
}
async function lookupHead(cwd: string, signal: AbortSignal): Promise<string | null> {
const res = await Process.text(["git", "rev-parse", "HEAD"], {
cwd,
abort: signal,
nothrow: true,
timeout: 1_000,
})
if (res.code !== 0) return null
const head = res.text.trim()
if (!head) return null
return head
}
async function lookupParent(cwd: string, signal: AbortSignal): Promise<string | null> {
const res = await Process.text(["gh", "repo", "view", "--json", "nameWithOwner,parent"], {
cwd,
abort: signal,
nothrow: true,
timeout: 1_000,
})
if (res.code !== 0) return null
const text = res.text.trim()
if (!text) return null
try {
const data = JSON.parse(text) as Repo
if (typeof data.nameWithOwner !== "string") return null
if (!data.parent) return null
const parent =
typeof data.parent.nameWithOwner === "string"
? data.parent.nameWithOwner
: typeof data.parent.owner?.login === "string" && typeof data.parent.name === "string"
? `${data.parent.owner.login}/${data.parent.name}`
: null
if (!parent || parent === data.nameWithOwner) return null
return parent
} catch {
return null
}
}
async function lookupByHead(
cwd: string,
branch: string,
head: string,
repo: string,
signal: AbortSignal,
): Promise<Pr | null> {
const res = await Process.text(
[
"gh",
"pr",
"list",
"-R",
repo,
"--state",
"open",
"--head",
branch,
"--limit",
"10",
"--json",
"number,title,headRefOid",
],
{
cwd,
abort: signal,
nothrow: true,
timeout: 1_000,
},
)
if (res.code !== 0) return null
const text = res.text.trim()
if (!text) return null
return select(text, head)
}
async function lookupBySha(
cwd: string,
head: string,
repo: string | undefined,
signal: AbortSignal,
): Promise<Pr | null> {
const cmd = [
"gh",
"pr",
"list",
...(repo ? ["-R", repo] : []),
"--state",
"open",
"--search",
`${head} is:pr`,
"--limit",
"5",
"--json",
"number,title,headRefOid",
]
const res = await Process.text(cmd, {
cwd,
abort: signal,
nothrow: true,
timeout: 1_000,
})
if (res.code !== 0) return null
const text = res.text.trim()
if (!text) return null
return select(text, head)
}
function select(text: string, head: string): Pr | null {
try {
const items = JSON.parse(text) as Item[]
if (!Array.isArray(items) || items.length === 0) return null
// Only accept a PR whose HEAD matches ours exactly — avoids returning a
// random PR that merely references the SHA in a commit message.
for (const item of items) {
if (item.headRefOid !== head) continue
if (typeof item.number !== "number") continue
if (typeof item.title !== "string") continue
return { number: item.number, title: item.title }
}
return null
} catch {
return null
}
}
function probeGh(): string | null {
const now = Date.now()
if (ghPath !== undefined && now - ghProbeTime < GH_PROBE_TTL) return ghPath
ghPath = Bun.which("gh") ?? null
ghProbeTime = now
return ghPath
}
function View(props: { api: TuiPluginApi }) {
const theme = () => props.api.theme.current
const branch = createMemo(() => props.api.state.vcs?.branch)
const cwd = createMemo(() => props.api.state.path.directory)
// Primitive string key: createResource wraps source in createMemo and
// compares with === for equality, so the same inputs produce a stable key
// and the fetcher is not retriggered on every render.
const key = createMemo(() => {
const b = branch()
const d = cwd()
if (!b || !d) return false as const
return `${d}\0${b}`
})
const [pr] = createResource(key, async (k) => {
if (!k) return null
const [d, b] = k.split("\0")
if (!d || !b) return null
return lookup(d, b).catch(() => null)
})
// The wrapper <box> must be present unconditionally — the OpenTUI slot
// registry relies on a stable root node per plugin. Gating the whole tree
// on `<Show>` breaks the mount. Conditionally render only the inner text.
return (
<box>
<Show when={pr()}>
<text fg={theme().textMuted}>
PR #{pr()!.number} - {pr()!.title}
</text>
</Show>
</box>
)
}
const tui: TuiPlugin = async (api) => {
api.slots.register({
order: 50,
slots: {
sidebar_content(_ctx, _props) {
return <View api={api} />
},
},
})
}
const plugin: TuiPluginModule & { id: string } = { id, tui }
export default plugin
@@ -1,8 +1,22 @@
// kilocode_change - new file
import { Effect } from "effect"
import path from "path"
import { Permission } from "@/permission"
import { Flag } from "@/flag/flag"
import { Global } from "@/global"
import { ModelID, ProviderID } from "@/provider/schema"
import type { Session } from "../../session"
import type { Agent } from "../../agent/agent"
import type { Config } from "../../config"
import z from "zod"
// RATIONALE: Mirror narrow state slice Task tool consumes and ignore unrelated TUI fields.
const ModelState = z
.object({
model: z.record(z.string(), z.object({ providerID: ProviderID.zod, modelID: ModelID.zod })).optional(),
variant: z.record(z.string(), z.string().optional()).optional(),
})
.passthrough()
export namespace KiloTask {
/** Reject primary agents used as subagents */
@@ -35,4 +49,25 @@ export namespace KiloTask {
export function permissions(rules: Permission.Ruleset): Permission.Ruleset {
return [{ permission: "task", pattern: "*", action: "deny" }, ...rules]
}
/** Return saved CLI model for agent, if any. */
export const resolveModel = Effect.fn("KiloTask.resolveModel")(function* (name: string) {
if (Flag.KILO_CLIENT !== "cli") return undefined
const file = path.join(Global.Path.state, "model.json")
const state = yield* Effect.tryPromise({
try: () =>
Bun.file(file)
.text()
.then((raw) => ModelState.safeParse(JSON.parse(raw)))
.then((result) => (result.success ? result.data : undefined))
.catch(() => undefined),
catch: () => undefined,
})
const model = state?.model?.[name]
if (!model) return undefined
return {
...model,
variant: state?.variant?.[`${model.providerID}/${model.modelID}`],
}
})
}
+16 -2
View File
@@ -1217,12 +1217,22 @@ const layer: Layer.Layer<
database[providerID] = parsed
}
// kilocode_change start - load auths before env so OAuth plugins can override inherited credentials
const auths = yield* auth.all().pipe(Effect.orDie)
// load env
const envs = yield* env.all()
for (const [id, provider] of Object.entries(database)) {
const providerID = ProviderID.make(id)
if (disabled.has(providerID)) continue
// kilocode_change start - prefer explicit OAuth auth over inherited env credentials
if (
auths[providerID]?.type === "oauth" &&
plugins.some((x) => x.auth?.provider === providerID && x.auth.loader)
) {
continue
}
const apiKey = provider.env.map((item) => envs[item]).find(Boolean)
// kilocode_change end
if (!apiKey) continue
mergeProvider(providerID, {
source: "env",
@@ -1231,7 +1241,6 @@ const layer: Layer.Layer<
}
// load apikeys
const auths = yield* auth.all().pipe(Effect.orDie)
for (const [id, provider] of Object.entries(auths)) {
const providerID = ProviderID.make(id)
if (disabled.has(providerID)) continue
@@ -1291,8 +1300,13 @@ const layer: Layer.Layer<
// load config - re-apply with updated data
for (const [id, provider] of configProviders) {
const providerID = ProviderID.make(id)
const partial: Partial<Info> = { source: "config" }
// kilocode_change start - keep OAuth plugin source when config and Codex auth coexist
const oauth =
auths[providerID]?.type === "oauth" &&
plugins.some((x) => x.auth?.provider === providerID && x.auth.loader)
const partial: Partial<Info> = oauth ? {} : { source: "config" }
if (provider.env) partial.env = provider.env
// kilocode_change end
if (provider.name) partial.name = provider.name
if (provider.options) partial.options = provider.options
mergeProvider(providerID, partial)
+54 -18
View File
@@ -183,7 +183,34 @@ function normalizeMessages(
return result
}
if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) {
// kilocode_change start - cherry-picked from anomalyco/opencode#24180;
// will be reverted on the next wholesale upstream merge.
// Deepseek requires all assistant messages to have reasoning on them
if (model.api.id.includes("deepseek")) {
msgs = msgs.map((msg) => {
if (msg.role !== "assistant") return msg
if (Array.isArray(msg.content)) {
if (msg.content.some((part) => part.type === "reasoning")) return msg
return { ...msg, content: [...msg.content, { type: "reasoning", text: "" }] }
}
return {
...msg,
content: [
...(msg.content ? [{ type: "text" as const, text: msg.content }] : []),
{ type: "reasoning" as const, text: "" },
],
}
})
}
// kilocode_change end
// kilocode_change start - cherry-picked from anomalyco/opencode#24435
if (
typeof model.capabilities.interleaved === "object" &&
model.capabilities.interleaved.field &&
model.api.npm !== "@openrouter/ai-sdk-provider"
) {
// kilocode_change end
const field = model.capabilities.interleaved.field
return msgs.map((msg) => {
if (msg.role === "assistant" && Array.isArray(msg.content)) {
@@ -193,25 +220,23 @@ function normalizeMessages(
// Filter out reasoning parts from content
const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
// Include reasoning_content | reasoning_details directly on the message for all assistant messages
if (reasoningText) {
return {
...msg,
content: filteredContent,
providerOptions: {
...msg.providerOptions,
openaiCompatible: {
...msg.providerOptions?.openaiCompatible,
[field]: reasoningText,
},
},
}
}
// kilocode_change start - cherry-picked from anomalyco/opencode#24146;
// will be reverted on the next wholesale upstream merge.
// Include reasoning_content | reasoning_details directly on the message for all assistant messages.
// Always set the field even when empty — some providers (e.g. DeepSeek) may return empty
// reasoning_content which still needs to be sent back in subsequent requests.
return {
...msg,
content: filteredContent,
providerOptions: {
...msg.providerOptions,
openaiCompatible: {
...msg.providerOptions?.openaiCompatible,
[field]: reasoningText,
},
},
}
// kilocode_change end
}
return msg
@@ -427,7 +452,12 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
if (
id.includes("deepseek") ||
// kilocode_change start - cherry-picked from anomalyco/opencode#24157
id.includes("deepseek-chat") ||
id.includes("deepseek-reasoner") ||
id.includes("deepseek-r1") ||
id.includes("deepseek-v3") ||
// kilocode_change end
id.includes("minimax") ||
// id.includes("glm") || // kilocode_change
id.includes("mistral") ||
@@ -571,7 +601,13 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
case "venice-ai-sdk-provider":
// https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
case "@ai-sdk/openai-compatible":
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
// kilocode_change start - cherry-picked from anomalyco/opencode#24163
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
if (model.api.id.includes("deepseek-v4")) {
efforts.push("max")
}
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
// kilocode_change end
case "@ai-sdk/azure":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
+11 -5
View File
@@ -792,34 +792,41 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const shellName = (
process.platform === "win32" ? path.win32.basename(sh, ".exe") : path.basename(sh)
).toLowerCase()
const cwd = ctx.directory // kilocode_change - moved up to use in invocations below
const invocations: Record<string, { args: string[] }> = {
nu: { args: ["-c", input.command] },
fish: { args: ["-c", input.command] },
zsh: {
// kilocode_change start - port anomalyco/opencode#24215: pass cwd as positional arg instead of $PWD (CI resets $PWD after startup files)
args: [
"-l",
"-c",
`
__oc_cwd=$PWD
[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
cd "$__oc_cwd"
cd -- "$1"
eval ${JSON.stringify(input.command)}
`,
"opencode",
cwd,
],
// kilocode_change end
},
bash: {
// kilocode_change start - port anomalyco/opencode#24215: pass cwd as positional arg instead of $PWD (CI resets $PWD after startup files)
args: [
"-l",
"-c",
`
__oc_cwd=$PWD
shopt -s expand_aliases
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
cd "$__oc_cwd"
cd -- "$1"
eval ${JSON.stringify(input.command)}
`,
"opencode",
cwd,
],
// kilocode_change end
},
cmd: { args: ["/c", input.command] },
powershell: { args: ["-NoProfile", "-Command", input.command] },
@@ -828,7 +835,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}
const args = (invocations[shellName] ?? invocations[""]).args
const cwd = ctx.directory
const shellEnv = yield* plugin.trigger(
"shell.env",
{ cwd, sessionID: input.sessionID, callID: part.callID },
@@ -3,7 +3,6 @@ import { Effect } from "effect"
import { EffectLogger } from "@/effect"
import { InstanceState } from "@/effect"
import type * as Tool from "./tool"
import { Instance } from "../project/instance"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
type Kind = "file" | "directory"
@@ -13,6 +12,16 @@ type Options = {
kind?: Kind
}
// kilocode_change start - root boundaries must not auto-allow external_directory
function root(dir: string) {
return path.parse(dir).root === dir
}
function inside(dir: string, file: string) {
return !root(dir) && AppFileSystem.contains(dir, file)
}
// kilocode_change end
export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirectory")(function* (
ctx: Tool.Context,
target?: string,
@@ -24,7 +33,9 @@ export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirec
const ins = yield* InstanceState.context
const full = process.platform === "win32" ? AppFileSystem.normalizePath(target) : target
if (Instance.containsPath(full, ins)) return
// kilocode_change start - keep root-workspace behavior intact outside permission prompts
if (inside(ins.directory, full) || inside(ins.worktree, full)) return
// kilocode_change end
const kind = options?.kind ?? "file"
const dir = kind === "directory" ? full : path.dirname(full)
+12 -4
View File
@@ -112,16 +112,22 @@ export const TaskTool = Tool.define(
const msg = yield* Effect.sync(() => MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }))
if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message"))
const model = next.model ?? {
modelID: msg.info.modelID,
providerID: msg.info.providerID,
}
// kilocode_change start — prefer user's CLI-saved pick for this subagent
const saved = yield* KiloTask.resolveModel(next.name)
const model = saved ??
next.model ?? {
modelID: msg.info.modelID,
providerID: msg.info.providerID,
}
const variant = saved?.variant ?? (saved ? undefined : next.variant)
// kilocode_change end
yield* ctx.metadata({
title: params.description,
metadata: {
sessionId: nextSession.id,
model,
variant, // kilocode_change
},
})
@@ -148,6 +154,7 @@ export const TaskTool = Tool.define(
modelID: model.modelID,
providerID: model.providerID,
},
variant, // kilocode_change
agent: next.name,
tools: {
...(canTodo ? {} : { todowrite: false }),
@@ -162,6 +169,7 @@ export const TaskTool = Tool.define(
metadata: {
sessionId: nextSession.id,
model,
variant, // kilocode_change
},
output: [
`task_id: ${nextSession.id} (for resuming to continue this task if needed)`,
+7 -2
View File
@@ -1,7 +1,9 @@
import { chmod, mkdir, readFile, stat as statFile, writeFile } from "fs/promises"
import { createWriteStream, existsSync, statSync } from "fs"
import { realpathSync } from "fs"
import { dirname, join, relative, resolve as pathResolve, win32 } from "path"
// kilocode_change start - harden containment checks
import { dirname, isAbsolute, join, relative, resolve as pathResolve, sep, win32 } from "path"
// kilocode_change end
import { Readable } from "stream"
import { pipeline } from "stream/promises"
import { Glob } from "@opencode-ai/shared/util/glob"
@@ -162,7 +164,10 @@ export function overlaps(a: string, b: string) {
}
export function contains(parent: string, child: string) {
return !relative(parent, child).startsWith("..")
// kilocode_change start - reject cross-drive and escaped relative paths
const rel = relative(parent, child)
return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`))
// kilocode_change end
}
export async function findUp(
@@ -0,0 +1,95 @@
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import path from "path"
import type { Permission } from "../../src/permission"
import { Instance } from "../../src/project/instance"
import { SessionID, MessageID } from "../../src/session/schema"
import { assertExternalDirectory } from "../../src/tool/external-directory"
import type { Tool } from "../../src/tool"
import { Filesystem } from "../../src/util"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { tmpdir } from "../fixture/fixture"
const base: Omit<Tool.Context, "ask"> = {
sessionID: SessionID.make("ses_test-boundary-session"),
messageID: MessageID.make(""),
callID: "",
agent: "code",
abort: AbortSignal.any([]),
messages: [],
metadata: () => Effect.void,
}
const glob = (p: string) =>
process.platform === "win32" ? AppFileSystem.normalizePathPattern(p) : p.replaceAll("\\", "/")
const asks = () => {
const items: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
const ctx: Tool.Context = {
...base,
ask: (req) =>
Effect.sync(() => {
items.push(req)
}),
}
return { items, ctx }
}
describe("kilocode external directory boundaries", () => {
test("asks before accessing outside a repo-root session", async () => {
await using repo = await tmpdir({ git: true })
await using outer = await tmpdir()
const file = path.join(outer.path, "outside.txt")
const { items, ctx } = asks()
await Instance.provide({
directory: repo.path,
fn: async () => {
try {
await assertExternalDirectory(ctx, file)
} finally {
await Instance.dispose()
}
},
})
const ext = items.find((item) => item.permission === "external_directory")
expect(ext).toBeDefined()
expect(ext!.patterns).toEqual([glob(path.join(outer.path, "*"))])
expect(ext!.always).toEqual([glob(path.join(outer.path, "*"))])
expect(ext!.metadata).toMatchObject({ filepath: file, parentDir: outer.path })
})
test("asks when the instance directory is a filesystem root", async () => {
await using outer = await tmpdir()
const root = path.parse(outer.path).root
const file = path.join(outer.path, "outside-root.txt")
const { items, ctx } = asks()
await Instance.provide({
directory: root,
fn: async () => {
try {
await assertExternalDirectory(ctx, file)
} finally {
await Instance.dispose()
}
},
})
const ext = items.find((item) => item.permission === "external_directory")
expect(ext).toBeDefined()
expect(ext!.patterns).toEqual([glob(path.join(outer.path, "*"))])
expect(ext!.metadata).toMatchObject({ filepath: file, parentDir: outer.path })
})
test("contains helpers keep dot-prefixed child names internal", () => {
expect(Filesystem.contains("/project", "/project/..cache/file")).toBe(true)
expect(AppFileSystem.contains("/a/b", "/a/b/..cache/file")).toBe(true)
})
test("AppFileSystem.contains rejects cross-drive paths on Windows", () => {
if (process.platform !== "win32") return
expect(AppFileSystem.contains("C:\\repo", "D:\\outside\\file.txt")).toBe(false)
})
})
@@ -63,6 +63,21 @@ function hasText(msg: Awaited<ReturnType<typeof SessionPrompt.prompt>>, text: st
return msg.parts.some((part) => part.type === "text" && part.text.includes(text))
}
// Find the last non-system message in an OpenAI-compatible request body. Kept
// tolerant: we only care about role invariants, not the exact content shape,
// because providers may serialize `content` as a string or as a parts array.
function lastConversational(body: Record<string, unknown>): { role: string; content: unknown } | undefined {
const msgs = Array.isArray(body.messages) ? (body.messages as Array<Record<string, unknown>>) : []
for (let i = msgs.length - 1; i >= 0; i--) {
const msg = msgs[i]
if (!msg || typeof msg !== "object") continue
const role = typeof msg.role === "string" ? msg.role : undefined
if (!role || role === "system") continue
return { role, content: msg.content }
}
return undefined
}
function user(sessionID: SessionID, id: MessageID): MessageV2.WithParts {
return {
info: {
@@ -303,18 +318,21 @@ describe("session prompt queue", () => {
const ready = Promise.withResolvers<void>()
const injected = Promise.withResolvers<void>()
const calls: number[] = []
const bodies: Array<Record<string, unknown>> = []
const server = Bun.serve({
port: 0,
fetch(req) {
async fetch(req) {
const url = new URL(req.url)
if (!url.pathname.endsWith("/chat/completions")) return new Response("not found", { status: 404 })
const body = (await req.json().catch(() => ({}))) as Record<string, unknown>
bodies.push(body)
calls.push(Date.now())
const body =
const stream =
calls.length === 1
? reply({ text: "first reply", ready: ready.resolve })
: reply({ text: "second reply", ready: injected.resolve })
return new Response(body, {
return new Response(stream, {
status: 200,
headers: { "Content-Type": "text/event-stream" },
})
@@ -408,6 +426,18 @@ describe("session prompt queue", () => {
}
expect(firstReply.info.parentID).toBe(firstUser.info.id)
expect(secondReply.info.parentID).toBe(secondUser.info.id)
// Regression for #9492: the second LLM request must end with the
// queued user prompt, not an assistant tail from the prior turn.
// Anthropic's API rejects requests whose final message is assistant
// (prefill), and scope() is supposed to partition the queued target
// turn to the end before the model request is built.
expect(bodies).toHaveLength(2)
const second2 = bodies[1]
expect(JSON.stringify(second2)).toContain("second prompt")
const tail = lastConversational(second2)
expect(tail?.role).toBe("user")
expect(JSON.stringify(tail?.content)).toContain("second prompt")
},
})
} finally {
@@ -0,0 +1,352 @@
import { afterEach, beforeAll, describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import fs from "fs/promises"
import path from "path"
import { Agent } from "../../src/agent/agent"
import { Config } from "../../src/config"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Global } from "../../src/global"
import { Instance } from "../../src/project/instance"
import { Session } from "../../src/session"
import { MessageV2 } from "../../src/session/message-v2"
import type { SessionPrompt } from "../../src/session/prompt"
import { MessageID, PartID } from "../../src/session/schema"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { TaskTool, type TaskPromptOps } from "../../src/tool/task"
import { Truncate } from "../../src/tool"
import { ToolRegistry } from "../../src/tool"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const state = path.join(Global.Path.state, "model.json")
afterEach(async () => {
process.env.KILO_CLIENT = "cli"
await fs.rm(state, { force: true }).catch(() => undefined)
await Instance.disposeAll()
})
beforeAll(async () => {
process.env.KILO_CLIENT = "cli"
await fs.rm(state, { force: true }).catch(() => undefined)
})
const parent = {
providerID: ProviderID.make("parent-provider"),
modelID: ModelID.make("parent-model"),
}
const saved = {
providerID: ProviderID.make("saved-provider"),
modelID: ModelID.make("saved-model"),
}
const cfg = {
providerID: ProviderID.make("config-provider"),
modelID: ModelID.make("config-model"),
}
const savedVariant = "fast"
const cfgVariant = "balanced"
const it = testEffect(
Layer.mergeAll(
Agent.defaultLayer,
Config.defaultLayer,
CrossSpawnSpawner.defaultLayer,
Session.defaultLayer,
Truncate.defaultLayer,
ToolRegistry.defaultLayer,
),
)
const seed = Effect.fn("TaskToolModelTest.seed")(function* (title = "Parent") {
const session = yield* Session.Service
const chat = yield* session.create({ title })
const user = yield* session.updateMessage({
id: MessageID.ascending(),
role: "user",
sessionID: chat.id,
agent: "build",
model: parent,
time: { created: Date.now() },
})
const assistant: MessageV2.Assistant = {
id: MessageID.ascending(),
role: "assistant",
parentID: user.id,
sessionID: chat.id,
mode: "build",
agent: "build",
cost: 0,
path: { cwd: "/tmp", root: "/tmp" },
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
modelID: parent.modelID,
providerID: parent.providerID,
time: { created: Date.now() },
}
yield* session.updateMessage(assistant)
return { chat, assistant }
})
function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void; text?: string }): TaskPromptOps {
return {
cancel() {},
resolvePromptParts: (template) => Effect.succeed([{ type: "text" as const, text: template }]),
prompt: (input) =>
Effect.sync(() => {
opts?.onPrompt?.(input)
return reply(input, opts?.text ?? "done")
}),
}
}
function reply(input: SessionPrompt.PromptInput, text: string): MessageV2.WithParts {
const id = MessageID.ascending()
return {
info: {
id,
role: "assistant",
parentID: input.messageID ?? MessageID.ascending(),
sessionID: input.sessionID,
mode: input.agent ?? "general",
agent: input.agent ?? "general",
cost: 0,
path: { cwd: "/tmp", root: "/tmp" },
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
modelID: input.model?.modelID ?? parent.modelID,
providerID: input.model?.providerID ?? parent.providerID,
time: { created: Date.now() },
finish: "stop",
},
parts: [
{
id: PartID.ascending(),
messageID: id,
sessionID: input.sessionID,
type: "text",
text,
},
],
}
}
function writeState(input: unknown) {
return Effect.promise(async () => {
await fs.mkdir(Global.Path.state, { recursive: true })
await fs.writeFile(state, JSON.stringify(input))
})
}
function run(input: { agent: "pinned" | "worker"; state?: unknown; client?: string }) {
return provideTmpdirInstance(
() =>
Effect.gen(function* () {
process.env.KILO_CLIENT = input.client ?? "cli"
if (input.state) yield* writeState(input.state)
const { chat, assistant } = yield* seed(input.agent)
const tool = yield* TaskTool
const def = yield* tool.init()
let seen: SessionPrompt.PromptInput | undefined
const promptOps = stubOps({ onPrompt: (value) => (seen = value) })
const result = yield* def.execute(
{
description: `run ${input.agent}`,
prompt: "inspect resolution",
subagent_type: input.agent,
},
{
sessionID: chat.id,
messageID: assistant.id,
agent: "build",
abort: new AbortController().signal,
extra: { promptOps, bypassAgentCheck: true },
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
},
)
return {
prompt: seen?.model,
variant: seen?.variant,
model: result.metadata.model,
metadataVariant: result.metadata.variant,
}
}),
{
config: {
agent: {
worker: { mode: "subagent" },
pinned: { mode: "subagent", model: "config-provider/config-model", variant: cfgVariant },
},
},
},
)
}
describe("tool.task model resolution", () => {
it.live("saved model beats agent config for pinned", () =>
run({
agent: "pinned",
state: { model: { pinned: saved }, variant: { "saved-provider/saved-model": savedVariant } },
}).pipe(
Effect.tap((result) =>
Effect.sync(() => {
expect(result.prompt).toEqual(saved)
expect(result.variant).toEqual(savedVariant)
expect(result.model).toMatchObject({ ...saved, variant: savedVariant })
expect(result.metadataVariant).toEqual(savedVariant)
}),
),
),
)
it.live("saved model beats parent for worker", () =>
run({
agent: "worker",
state: { model: { worker: saved }, variant: { "saved-provider/saved-model": savedVariant } },
}).pipe(
Effect.tap((result) =>
Effect.sync(() => {
expect(result.prompt).toEqual(saved)
expect(result.variant).toEqual(savedVariant)
expect(result.model).toMatchObject({ ...saved, variant: savedVariant })
expect(result.metadataVariant).toEqual(savedVariant)
}),
),
),
)
it.live("saved model without variant leaves variant undefined", () =>
run({
agent: "worker",
state: { model: { worker: saved } },
}).pipe(
Effect.tap((result) =>
Effect.sync(() => {
expect(result.prompt).toEqual(saved)
expect(result.variant).toBeUndefined()
expect(result.model).toEqual(saved)
expect(result.metadataVariant).toBeUndefined()
}),
),
),
)
it.live("unrelated saved variant key ignored", () =>
run({
agent: "worker",
state: { model: { worker: saved }, variant: { "other-provider/other-model": savedVariant } },
}).pipe(
Effect.tap((result) =>
Effect.sync(() => {
expect(result.prompt).toEqual(saved)
expect(result.variant).toBeUndefined()
expect(result.model).toEqual(saved)
expect(result.metadataVariant).toBeUndefined()
}),
),
),
)
it.live("missing saved entry falls back to agent config for pinned", () =>
run({
agent: "pinned",
state: { model: { worker: saved } },
}).pipe(
Effect.tap((result) =>
Effect.sync(() => {
expect(result.prompt).toEqual(cfg)
expect(result.variant).toEqual(cfgVariant)
expect(result.model).toEqual(cfg)
expect(result.metadataVariant).toEqual(cfgVariant)
}),
),
),
)
it.live("no file and no agent config falls back to parent for worker", () =>
run({
agent: "worker",
}).pipe(
Effect.tap((result) =>
Effect.sync(() => {
expect(result.prompt).toEqual(parent)
expect(result.variant).toBeUndefined()
expect(result.model).toEqual(parent)
expect(result.metadataVariant).toBeUndefined()
}),
),
),
)
it.live("malformed file ignored and falls back to agent config for pinned", () =>
provideTmpdirInstance(
() =>
Effect.gen(function* () {
process.env.KILO_CLIENT = "cli"
yield* Effect.promise(async () => {
await fs.mkdir(Global.Path.state, { recursive: true })
await fs.writeFile(state, "{bad json")
})
const { chat, assistant } = yield* seed("pinned")
const tool = yield* TaskTool
const def = yield* tool.init()
let seen: SessionPrompt.PromptInput | undefined
const promptOps = stubOps({ onPrompt: (value) => (seen = value) })
const result = yield* def.execute(
{
description: "run pinned",
prompt: "inspect resolution",
subagent_type: "pinned",
},
{
sessionID: chat.id,
messageID: assistant.id,
agent: "build",
abort: new AbortController().signal,
extra: { promptOps, bypassAgentCheck: true },
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
},
)
expect(seen?.model).toEqual(cfg)
expect(seen?.variant).toEqual(cfgVariant)
expect(result.metadata.model).toEqual(cfg)
expect(result.metadata.variant).toEqual(cfgVariant)
}),
{
config: {
agent: {
worker: { mode: "subagent" },
pinned: { mode: "subagent", model: "config-provider/config-model", variant: cfgVariant },
},
},
},
),
)
it.live("non-CLI client gate ignores saved worker model and uses parent", () =>
run({
agent: "worker",
client: "vscode",
state: { model: { worker: saved }, variant: { "saved-provider/saved-model": savedVariant } },
}).pipe(
Effect.tap((result) =>
Effect.sync(() => {
expect(result.prompt).toEqual(parent)
expect(result.variant).toBeUndefined()
expect(result.model).toEqual(parent)
expect(result.metadataVariant).toBeUndefined()
}),
),
),
)
})
@@ -14,6 +14,7 @@ import { Env } from "../../src/env"
import { Effect } from "effect"
import { AppRuntime } from "../../src/effect/app-runtime"
import { makeRuntime } from "../../src/effect/run-service"
import { Auth } from "../../src/auth" // kilocode_change
const env = makeRuntime(Env.Service, Env.defaultLayer)
const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v))
@@ -88,6 +89,50 @@ test("provider loaded from env variable", async () => {
})
})
// kilocode_change start
test("provider OAuth auth overrides inherited env variable", async () => {
await Auth.remove("openai")
await Auth.set("openai", {
type: "oauth",
refresh: "test-refresh-token",
access: "test-access-token",
expires: Date.now() + 60_000,
})
try {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://app.kilo.ai/config.json",
}),
)
},
})
await Instance.provide({
directory: tmp.path,
init: async () => {
set("OPENAI_API_KEY", "test-openai-key")
},
fn: async () => {
const providers = await list()
const provider = providers[ProviderID.openai]
expect(provider).toBeDefined()
if (!provider) throw new Error("Expected OpenAI provider")
expect(provider.source).toBe("custom")
expect(provider.key).toBeUndefined()
expect(Object.values(provider.models).every((model) => model.cost.input === 0 && model.cost.output === 0)).toBe(
true,
)
},
})
} finally {
await Auth.remove("openai")
}
})
// kilocode_change end
test("provider loaded from config with apiKey option", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
@@ -803,6 +803,81 @@ describe("session.message-v2.toModelMessage", () => {
])
})
// kilocode_change start - cherry-picked from anomalyco/opencode#24435
test("preserves OpenRouter reasoning details through provider transform", async () => {
const assistantID = "m-assistant"
const openrouterModel: Provider.Model = {
...model,
id: ModelID.make("deepseek/deepseek-v4-pro"),
providerID: ProviderID.make("openrouter"),
api: {
id: "deepseek/deepseek-v4-pro",
url: "https://openrouter.ai/api/v1",
npm: "@openrouter/ai-sdk-provider",
},
capabilities: {
...model.capabilities,
reasoning: true,
interleaved: { field: "reasoning_details" },
},
}
const reasoningDetails = [
{
type: "reasoning.text",
text: "thinking",
format: "unknown",
index: 0,
},
]
const input: MessageV2.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent", undefined, {
providerID: openrouterModel.providerID,
modelID: openrouterModel.id,
}),
parts: [
{
...basePart(assistantID, "a1"),
type: "reasoning",
text: "thinking",
time: { start: 0 },
metadata: {
openrouter: {
reasoning_details: reasoningDetails,
},
},
},
{
...basePart(assistantID, "a2"),
type: "text",
text: "answer",
},
] as MessageV2.Part[],
},
]
expect(
ProviderTransform.message(await MessageV2.toModelMessages(input, openrouterModel), openrouterModel, {}),
).toStrictEqual([
{
role: "assistant",
content: [
{
type: "reasoning",
text: "thinking",
providerOptions: {
openrouter: {
reasoning_details: reasoningDetails,
},
},
},
{ type: "text", text: "answer" },
],
},
])
})
// kilocode_change end
test("splits assistant messages on step-start boundaries", async () => {
const assistantID = "m-assistant"
@@ -885,7 +885,13 @@ it.live(
10_000, // kilocode_change
)
// kilocode_change start - skip flaky test, tracked in #8990
// kilocode_change start - #9492: the upstream fork-based shape of this test
// loses Instance AsyncLocalStorage context in the second forked prompt, which
// surfaces as a "No context found for instance" die before the queue behavior
// can be exercised. The Kilo queue semantics (in-flight stream drains, second
// LLM request ends with the queued user message) are covered end-to-end in
// packages/opencode/test/kilocode/session-prompt-queue.test.ts — keep this
// upstream scaffold skipped so future OpenCode merges remain friction-free.
it.live.skip(
"prompt submitted during an active run is included in the next LLM input",
// kilocode_change end
@@ -1080,6 +1086,32 @@ unix("shell completes a fast command on the preferred shell", () =>
),
)
// kilocode_change start - port anomalyco/opencode#24215 cover shell cwd changes (agent "build" → "code" for our fork)
unix("shell commands can change directory after startup", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const { prompt, run, chat } = yield* boot()
const parent = path.dirname(dir)
const result = yield* prompt.shell({
sessionID: chat.id,
agent: "code",
command: "cd .. && pwd",
})
expect(result.info.role).toBe("assistant")
const tool = completedTool(result.parts)
if (!tool) return
expect(tool.state.output).toContain(parent)
expect(tool.state.metadata.output).toContain(parent)
yield* run.assertNotBusy(chat.id)
}),
{ git: true, config: cfg },
),
)
// kilocode_change end
unix("shell lists files from the project directory", () =>
provideTmpdirInstance(
(dir) =>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/plugin",
"version": "7.2.22",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -12,6 +12,6 @@
"exports": {
".": "./src/index.ts"
},
"version": "7.2.22",
"version": "7.2.25",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/sdk",
"version": "7.2.22",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -217,9 +217,9 @@ import type {
SyncHistoryListResponses,
SyncReplayErrors,
SyncReplayResponses,
SyncStartResponses,
TelemetryCaptureErrors,
TelemetryCaptureResponses,
SyncStartResponses,
TextPartInput,
ToolIdsErrors,
ToolIdsResponses,
+4 -4
View File
@@ -1703,10 +1703,6 @@ export type Config = {
* @deprecated Use 'share' field instead. Share newly created sessions automatically
*/
autoshare?: boolean
/**
* Enable remote control of sessions via Kilo Cloud. Equivalent to running /remote on startup.
*/
remote_control?: boolean
/**
* Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications
*/
@@ -1719,6 +1715,10 @@ export type Config = {
* When set, ONLY these providers will be enabled. All other providers will be ignored
*/
enabled_providers?: Array<string>
/**
* Enable remote control of sessions via Kilo Cloud. Equivalent to running /remote on startup.
*/
remote_control?: boolean
/**
* Model to use in the format of provider/model, eg anthropic/claude-2
*/
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.2.22",
"version": "7.2.25",
"name": "@opencode-ai/shared",
"type": "module",
"license": "MIT",
+12 -2
View File
@@ -1,5 +1,12 @@
import { NodeFileSystem } from "@effect/platform-node"
import { dirname, join, relative, resolve as pathResolve } from "path"
import {
dirname,
isAbsolute,
join,
relative,
resolve as pathResolve,
sep,
} from "path" // kilocode_change - harden containment checks
import { realpathSync } from "fs"
import * as NFS from "fs/promises"
import { lookup } from "mime-types"
@@ -231,6 +238,9 @@ export namespace AppFileSystem {
}
export function contains(parent: string, child: string) {
return !relative(parent, child).startsWith("..")
// kilocode_change start - reject cross-drive and escaped relative paths
const rel = relative(parent, child)
return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`))
// kilocode_change end
}
}
@@ -0,0 +1,13 @@
import { describe, expect, test } from "bun:test"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
describe("kilocode filesystem containment", () => {
test("keeps dot-prefixed child names internal", () => {
expect(AppFileSystem.contains("/a/b", "/a/b/..cache/file")).toBe(true)
})
test("rejects cross-drive paths on Windows", () => {
if (process.platform !== "win32") return
expect(AppFileSystem.contains("C:\\repo", "D:\\outside\\file.txt")).toBe(false)
})
})
+1 -1
View File
@@ -26,7 +26,7 @@
"typescript": "catalog:",
"vite": "catalog:"
},
"version": "7.2.22",
"version": "7.2.25",
"dependencies": {},
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "7.2.22",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/upstream-merge",
"version": "7.2.22",
"version": "7.2.25",
"private": true,
"type": "module",
"description": "Scripts for automating upstream opencode merges into Kilo",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "7.2.22",
"version": "7.2.25",
"publisher": "sst-dev",
"repository": {
"type": "git",