mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge remote-tracking branch 'origin/main' into docs/customize-tabs
This commit is contained in:
@@ -7,9 +7,9 @@ on:
|
||||
jobs:
|
||||
call-webhook:
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
(startsWith(github.event.pull_request.title, 'feat:') ||
|
||||
startsWith(github.event.pull_request.title, 'feat('))
|
||||
github.event.pull_request.merged == true &&
|
||||
(startsWith(github.event.pull_request.title, 'feat:') ||
|
||||
startsWith(github.event.pull_request.title, 'feat('))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send webhook safely
|
||||
|
||||
@@ -16,6 +16,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang
|
||||
- **Single test**: `bun test test/tool/tool.test.ts` from `packages/opencode/`
|
||||
- **SDK regen**: After changing server endpoints in `packages/opencode/src/server/`, run `./script/generate.ts` from root to regenerate `packages/sdk/js/`
|
||||
- **Knip** (unused exports): `bun run knip` from `packages/kilo-vscode/`. CI runs this — all exported types/functions must be imported somewhere. Remove or unexport unused exports before pushing.
|
||||
- **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.
|
||||
|
||||
## Products
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<p align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
|
||||
<a href="https://x.com/kilocode"><img src="https://img.shields.io/badge/kilocode-000000?style=flat&logo=x&logoColor=white" alt="X (Twitter)"></a>
|
||||
<a href="https://blog.kilo.ai"><img src="https://img.shields.io/badge/Blog-555?style=flat&logo=substack&logoColor=white" alt="Substack Blog"></a>
|
||||
<a href="https://kilo.ai/discord"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
|
||||
<a href="https://www.reddit.com/r/kilocode/"><img src="https://img.shields.io/badge/Join%20r%2Fkilocode-D84315?style=flat&logo=reddit&logoColor=white" alt="Reddit"></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code"><img src="https://raster.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
|
||||
<a href="https://x.com/kilocode"><img src="https://raster.shields.io/badge/kilocode-000000?style=flat&logo=x&logoColor=white" alt="X (Twitter)"></a>
|
||||
<a href="https://blog.kilo.ai"><img src="https://raster.shields.io/badge/Blog-555?style=flat&logo=substack&logoColor=white" alt="Substack Blog"></a>
|
||||
<a href="https://kilo.ai/discord"><img src="https://raster.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
|
||||
<a href="https://www.reddit.com/r/kilocode/"><img src="https://raster.shields.io/badge/Join%20r%2Fkilocode-D84315?style=flat&logo=reddit&logoColor=white" alt="Reddit"></a>
|
||||
</p>
|
||||
|
||||
# 🚀 Kilo
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-i18n": "workspace:*",
|
||||
"@kilocode/kilo-ui": "workspace:*",
|
||||
@@ -79,7 +79,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -112,7 +112,7 @@
|
||||
},
|
||||
"packages/desktop-electron": {
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -142,7 +142,7 @@
|
||||
},
|
||||
"packages/kilo-docs": {
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"dependencies": {
|
||||
"@docsearch/css": "^4",
|
||||
"@docsearch/js": "^4",
|
||||
@@ -171,8 +171,11 @@
|
||||
},
|
||||
"packages/kilo-gateway": {
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.65",
|
||||
"@ai-sdk/openai": "2.0.101",
|
||||
"@ai-sdk/openai-compatible": "1.0.32",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
@@ -203,7 +206,7 @@
|
||||
},
|
||||
"packages/kilo-i18n": {
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -213,7 +216,7 @@
|
||||
},
|
||||
"packages/kilo-telemetry": {
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
@@ -233,7 +236,7 @@
|
||||
},
|
||||
"packages/kilo-ui": {
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -268,7 +271,7 @@
|
||||
},
|
||||
"packages/kilo-vscode": {
|
||||
"name": "kilo-code",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@kilocode/kilo-i18n": "workspace:*",
|
||||
@@ -321,7 +324,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@kilocode/cli",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"bin": {
|
||||
"kilo": "./bin/kilo",
|
||||
"kilocode": "./bin/kilo",
|
||||
@@ -341,7 +344,7 @@
|
||||
"@ai-sdk/google-vertex": "3.0.106",
|
||||
"@ai-sdk/groq": "2.0.34",
|
||||
"@ai-sdk/mistral": "2.0.27",
|
||||
"@ai-sdk/openai": "2.0.89",
|
||||
"@ai-sdk/openai": "2.0.101",
|
||||
"@ai-sdk/openai-compatible": "1.0.32",
|
||||
"@ai-sdk/perplexity": "2.0.23",
|
||||
"@ai-sdk/provider": "2.0.1",
|
||||
@@ -443,7 +446,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -457,14 +460,14 @@
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"devDependencies": {
|
||||
"@types/bun": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.90.10",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -475,7 +478,7 @@
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
@@ -498,7 +501,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -545,7 +548,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -662,7 +665,7 @@
|
||||
|
||||
"@ai-sdk/mistral": ["@ai-sdk/mistral@2.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gaptHgaXjMw3+eA0Q4FABcsj5nQNP6EpFaGUR+Pj5WJy7Kn6mApl975/x57224MfeJIShNpt8wFKK3tvh5ewKg=="],
|
||||
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.101", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kQ52HLV45T3bQbRzWExXW6+pkg3Nvq4dUnZHUPJXWgkUUsAhZjxHrXqPOc/0yfn/4+Dn2uLmIgAkP9IfzMMcNg=="],
|
||||
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||
|
||||
@@ -4354,6 +4357,8 @@
|
||||
|
||||
"@ai-sdk/amazon-bedrock/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||
|
||||
"@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
|
||||
|
||||
"@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
||||
|
||||
"@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
||||
@@ -4378,7 +4383,7 @@
|
||||
|
||||
"@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
||||
|
||||
"@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
||||
"@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
|
||||
|
||||
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
||||
|
||||
@@ -4832,6 +4837,8 @@
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
@@ -5596,6 +5603,8 @@
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
||||
|
||||
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"app-builder-lib/@electron/get/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=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-h4RHYUp03Kpy2GCPv5B7pjfcHxZ9M9R475svzkwzpH4=",
|
||||
"aarch64-linux": "sha256-zLCL5Z5NRvWBfsxnjdMGEWAYBNU120G2/YQ0KFaq+ZA=",
|
||||
"aarch64-darwin": "sha256-KZPpNosE6S3rW0RSCiADi26ysPIt74TeULQdYF7dDOE=",
|
||||
"x86_64-darwin": "sha256-XkEYBrRiD1DRhWtQ73afzBNRk0bZHRFpxqAcwnsraE8="
|
||||
"x86_64-linux": "sha256-YjB3YfaA79nztpZoYEAlFBakS//G/Guprlqk4BYFgOA=",
|
||||
"aarch64-linux": "sha256-ru470M8RVwyOPNGr6I4VfApQRHhYFFMPjOBneTRBJtc=",
|
||||
"aarch64-darwin": "sha256-M6LIPBHnYH9AtIRAKyW44VN71Kb28nztzFteCkL0588=",
|
||||
"x86_64-darwin": "sha256-HURoot79BFHYwoEp0XqVdI6cRdV7KGvmRRqOf+hUa9Q="
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -112,6 +112,6 @@
|
||||
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch",
|
||||
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
|
||||
},
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -127,7 +127,7 @@ const INTL: Record<Locale, string> = {
|
||||
th: "th",
|
||||
bs: "bs",
|
||||
nl: "nl",
|
||||
|
||||
|
||||
tr: "tr",
|
||||
}
|
||||
|
||||
@@ -149,7 +149,6 @@ const LABEL_KEY: Record<Locale, keyof Dictionary> = {
|
||||
th: "language.th",
|
||||
bs: "language.bs",
|
||||
nl: "language.nl",
|
||||
|
||||
tr: "language.tr",
|
||||
}
|
||||
|
||||
|
||||
@@ -136,8 +136,7 @@ export const dict = {
|
||||
"provider.connect.oauth.code.invalid": "رمز التفويض غير صالح",
|
||||
"provider.connect.oauth.auto.visit.prefix": "قم بزيارة ",
|
||||
"provider.connect.oauth.auto.visit.link": "هذا الرابط",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.",
|
||||
"provider.connect.oauth.auto.visit.suffix": " وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.",
|
||||
"provider.connect.oauth.auto.confirmationCode": "رمز التأكيد",
|
||||
"provider.connect.toast.connected.title": "تم توصيل {{provider}}",
|
||||
"provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.",
|
||||
|
||||
@@ -389,8 +389,7 @@ export const dict = {
|
||||
"toast.session.unshare.failed.description": "Une erreur s'est produite lors de l'annulation du partage de la session",
|
||||
"toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}",
|
||||
"toast.update.title": "Mise à jour disponible",
|
||||
"toast.update.description":
|
||||
"Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.",
|
||||
"toast.update.description": "Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.",
|
||||
"toast.update.action.installRestart": "Installer et redémarrer",
|
||||
"toast.update.action.notYet": "Pas encore",
|
||||
"error.page.title": "Quelque chose s'est mal passé",
|
||||
@@ -525,8 +524,7 @@ export const dict = {
|
||||
"sidebar.workspaces.enable": "Activer les espaces de travail",
|
||||
"sidebar.workspaces.disable": "Désactiver les espaces de travail",
|
||||
"sidebar.gettingStarted.title": "Commencer",
|
||||
"sidebar.gettingStarted.line1":
|
||||
"Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.",
|
||||
"sidebar.gettingStarted.line1": "Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.",
|
||||
"sidebar.gettingStarted.line2":
|
||||
"Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.",
|
||||
"sidebar.project.recentSessions": "Sessions récentes",
|
||||
|
||||
@@ -148,8 +148,7 @@ export const dict = {
|
||||
"provider.connect.oauth.code.invalid": "รหัสการอนุญาตไม่ถูกต้อง",
|
||||
"provider.connect.oauth.auto.visit.prefix": "เยี่ยมชม ",
|
||||
"provider.connect.oauth.auto.visit.link": "ลิงก์นี้",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo",
|
||||
"provider.connect.oauth.auto.visit.suffix": " และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo",
|
||||
"provider.connect.oauth.auto.confirmationCode": "รหัสยืนยัน",
|
||||
"provider.connect.toast.connected.title": "{{provider}} ที่เชื่อมต่อแล้ว",
|
||||
"provider.connect.toast.connected.description": "โมเดล {{provider}} พร้อมใช้งานแล้ว",
|
||||
|
||||
@@ -143,8 +143,7 @@ export const dict = {
|
||||
"provider.connect.opencodeZen.visit.prefix": "",
|
||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||
"provider.connect.opencodeZen.visit.suffix": " adresini ziyaret ederek API anahtarınızı alın.",
|
||||
"provider.connect.oauth.code.visit.prefix":
|
||||
"Hesabınızı bağlamak ve Kilo'da {{provider}} modellerini kullanmak için ",
|
||||
"provider.connect.oauth.code.visit.prefix": "Hesabınızı bağlamak ve Kilo'da {{provider}} modellerini kullanmak için ",
|
||||
"provider.connect.oauth.code.visit.link": "bu bağlantıya",
|
||||
"provider.connect.oauth.code.visit.suffix": " tıklayarak yetkilendirme kodunuzu alın.",
|
||||
"provider.connect.oauth.code.label": "{{method}} yetkilendirme kodu",
|
||||
|
||||
@@ -149,8 +149,7 @@ export const dict = {
|
||||
"provider.connect.oauth.code.invalid": "授權碼無效",
|
||||
"provider.connect.oauth.auto.visit.prefix": "造訪 ",
|
||||
"provider.connect.oauth.auto.visit.link": "此連結",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。",
|
||||
"provider.connect.oauth.auto.visit.suffix": " 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。",
|
||||
"provider.connect.oauth.auto.confirmationCode": "確認碼",
|
||||
"provider.connect.toast.connected.title": "{{provider}} 已連線",
|
||||
"provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。",
|
||||
|
||||
@@ -43,9 +43,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
|
||||
<div class="size-full rounded overflow-clip">
|
||||
<Avatar
|
||||
fallback={name()}
|
||||
src={
|
||||
props.project.id === KILO_PROJECT_ID ? "https://kilo.ai/favicon.svg" : props.project.icon?.override
|
||||
}
|
||||
src={props.project.id === KILO_PROJECT_ID ? "https://kilo.ai/favicon.svg" : props.project.icon?.override}
|
||||
{...getAvatarColors(props.project.icon?.color)}
|
||||
class="size-full rounded"
|
||||
classList={{ "badge-mask": notify() }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -45,8 +45,7 @@ export const dict = {
|
||||
"desktop.menu.help.shareFeedback": "Feedback teilen",
|
||||
"desktop.menu.help.reportBug": "Einen Fehler melden",
|
||||
"desktop.cli.error.unsupportedPlatform": "Die CLI-Installation wird nur unter macOS und Linux unterstützt.",
|
||||
"desktop.cli.error.sidecarMissing":
|
||||
"Das Kilo CLI-Binary fehlt. Versuchen Sie, die Desktop-App neu zu installieren.",
|
||||
"desktop.cli.error.sidecarMissing": "Das Kilo CLI-Binary fehlt. Versuchen Sie, die Desktop-App neu zu installieren.",
|
||||
"desktop.cli.error.scriptWriteFailed": "Das CLI-Installationsskript konnte nicht vorbereitet werden.",
|
||||
"desktop.cli.error.scriptPermissionFailed": "Das CLI-Installationsskript konnte nicht ausführbar gemacht werden.",
|
||||
"desktop.cli.error.scriptRunFailed": "Das CLI-Installationsskript konnte nicht ausgeführt werden.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.1.2"
|
||||
version = "7.1.8"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/Kilo-Org/kilocode"
|
||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.2/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.8/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.2/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.8/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.2/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.8/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.2/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.8/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.2/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.8/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from "react"
|
||||
import type { Platform } from "../lib/types"
|
||||
|
||||
interface Props {
|
||||
platform?: Platform
|
||||
}
|
||||
|
||||
export function PageVersionSwitcher({ platform }: Props) {
|
||||
if (!platform || platform === "all") return null
|
||||
|
||||
const legacy = platform === "legacy"
|
||||
return (
|
||||
<div className="version-banner">
|
||||
<span className="version-banner-icon">{legacy ? "\u24D8" : "\u2728"}</span>
|
||||
<span>
|
||||
{legacy
|
||||
? "This page applies to the legacy VSCode extension."
|
||||
: "This page applies to the current VSCode extension & CLI."}
|
||||
</span>
|
||||
|
||||
<style jsx>{`
|
||||
.version-banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.625rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.version-banner-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import Link from "next/link"
|
||||
import { SectionNav } from "../lib/types"
|
||||
import { Platform, SectionNav } from "../lib/types"
|
||||
import { Nav } from "../lib/nav"
|
||||
|
||||
// Define navigation items for each major section
|
||||
@@ -67,6 +67,16 @@ const ChevronLeft = () => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
function PlatformBadge({ platform }: { platform?: Platform }) {
|
||||
if (!platform || platform === "all") return null
|
||||
const label = platform === "legacy" ? "Legacy" : "New"
|
||||
return (
|
||||
<span className="platform-badge" data-platform={platform}>
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
interface SideNavProps {
|
||||
isMobileOpen?: boolean
|
||||
onMobileClose?: () => void
|
||||
@@ -268,6 +278,7 @@ export function SideNav({ isMobileOpen = false, onMobileClose }: SideNavProps) {
|
||||
<div className="nav-item-with-toggle">
|
||||
<Link href={link.href} onClick={handleLinkClick}>
|
||||
{link.children}
|
||||
<PlatformBadge platform={link.platform} />
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
@@ -290,6 +301,7 @@ export function SideNav({ isMobileOpen = false, onMobileClose }: SideNavProps) {
|
||||
<li key={subLink.href} className={subActive ? "active" : ""}>
|
||||
<Link href={subLink.href} onClick={handleLinkClick}>
|
||||
{subLink.children}
|
||||
<PlatformBadge platform={subLink.platform} />
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
@@ -300,6 +312,7 @@ export function SideNav({ isMobileOpen = false, onMobileClose }: SideNavProps) {
|
||||
) : (
|
||||
<Link href={link.href} onClick={handleLinkClick}>
|
||||
{link.children}
|
||||
<PlatformBadge platform={link.platform} />
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
@@ -478,6 +491,29 @@ export function SideNav({ isMobileOpen = false, onMobileClose }: SideNavProps) {
|
||||
color: var(--accent-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-links :global(.platform-badge) {
|
||||
display: inline-block;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
padding: 0.0625rem 0.3125rem;
|
||||
border-radius: 0.1875rem;
|
||||
margin-left: 0.375rem;
|
||||
vertical-align: middle;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.nav-links :global(.platform-badge[data-platform="legacy"]) {
|
||||
background-color: rgba(139, 92, 246, 0.12);
|
||||
color: rgb(139, 92, 246);
|
||||
}
|
||||
|
||||
.nav-links :global(.platform-badge[data-platform="new"]) {
|
||||
background-color: rgba(34, 197, 94, 0.12);
|
||||
color: rgb(34, 197, 94);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -27,16 +27,16 @@ export function Tabs({ children }: TabsProps) {
|
||||
)
|
||||
|
||||
const indexFromHash = () => {
|
||||
if (typeof window === "undefined") return 0
|
||||
const hash = window.location.hash.slice(1)
|
||||
if (!hash) return 0
|
||||
const found = tabs.findIndex((tab) => slugify(tab.props.label) === hash)
|
||||
return found >= 0 ? found : 0
|
||||
}
|
||||
|
||||
const [activeIndex, setActiveIndex] = useState(indexFromHash)
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
setActiveIndex(indexFromHash())
|
||||
const onHashChange = () => setActiveIndex(indexFromHash())
|
||||
window.addEventListener("hashchange", onHashChange)
|
||||
return () => window.removeEventListener("hashchange", onHashChange)
|
||||
|
||||
@@ -336,9 +336,8 @@ export function TopNav({ onMobileMenuToggle, isMobileMenuOpen = false, showMobil
|
||||
{/* Announcement banner */}
|
||||
<div className="announcement-banner">
|
||||
<p>
|
||||
We're <Link href="https://blog.kilo.ai/p/kilo-cli">replatforming our extensions on the new Kilo CLI</Link>.
|
||||
Contribute to the new CLI and pre-release extensions at{" "}
|
||||
<Link href="https://github.com/Kilo-Org/kilocode">Kilo-Org/kilocode</Link>.
|
||||
We've <Link href="https://blog.kilo.ai/p/kilo-cli">replatformed our extensions on the new Kilo CLI</Link>.
|
||||
Contribute at <Link href="https://github.com/Kilo-Org/kilocode">Kilo-Org/kilocode</Link>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from "./Icon"
|
||||
export * from "./Image"
|
||||
export * from "./KiloCodeIcon"
|
||||
export * from "./PageFooter"
|
||||
export * from "./PageVersionSwitcher"
|
||||
export * from "./SideNav"
|
||||
export * from "./Table"
|
||||
export * from "./TableOfContents"
|
||||
|
||||
@@ -30,8 +30,12 @@ export const GettingStartedNav: NavSection[] = [
|
||||
children: "Settings",
|
||||
subLinks: [
|
||||
{ href: "/getting-started/settings/auto-approving-actions", children: "Auto-Approving Actions" },
|
||||
{ href: "/getting-started/settings/auto-cleanup", children: "Auto Cleanup" },
|
||||
{ href: "/getting-started/settings/system-notifications", children: "System Notifications" },
|
||||
{ href: "/getting-started/settings/auto-cleanup", children: "Auto Cleanup", platform: "legacy" },
|
||||
{
|
||||
href: "/getting-started/settings/system-notifications",
|
||||
children: "System Notifications",
|
||||
platform: "legacy",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ href: "/getting-started/adding-credits", children: "Adding Credits" },
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export type Platform = "legacy" | "new" | "all"
|
||||
|
||||
export interface NavLink {
|
||||
href: string
|
||||
children: string
|
||||
platform?: Platform // "legacy" = stable VSCode only, "new" = new VSCode + CLI only, omitted = universal
|
||||
subLinks?: NavLink[] // Optional nested links for second-level navigation
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Before installing the Kilo Code plugin, ensure you have:
|
||||
1. If you don't have a JetBrains IDE installed, download one from [jetbrains.com](https://www.jetbrains.com/)
|
||||
2. Then, you can click the button below to install Kilo Code directly from the JetBrains Marketplace:
|
||||
|
||||
[](https://plugins.jetbrains.com/plugin/28350-kilo-code)
|
||||
[](https://plugins.jetbrains.com/plugin/28350-kilo-code)
|
||||
|
||||
### Install from JetBrains Marketplace
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
1. If you don't have VS Code installed, download it from [code.visualstudio.com](https://code.visualstudio.com/)
|
||||
2. Then, you can click the button below to install Kilo Code directly from the VS Code Marketplace:
|
||||
|
||||
[](vscode:extension/kilocode.kilo-code)
|
||||
[](vscode:extension/kilocode.kilo-code)
|
||||
|
||||
### Install from VS Code Marketplace
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -3,7 +3,7 @@ import Head from "next/head"
|
||||
import { useRouter } from "next/router"
|
||||
import posthog from "posthog-js"
|
||||
|
||||
import { CopyPageButton, PageFooter, SideNav, TableOfContents, TopNav } from "../components"
|
||||
import { CopyPageButton, PageFooter, PageVersionSwitcher, SideNav, TableOfContents, TopNav } from "../components"
|
||||
|
||||
import "prismjs"
|
||||
import "prismjs/components/prism-bash.min"
|
||||
@@ -129,6 +129,8 @@ export default function MyApp({ Component, pageProps }: AppProps<MyAppProps>) {
|
||||
|
||||
const toc = pageProps.markdoc?.content ? collectHeadings(pageProps.markdoc.content) : []
|
||||
|
||||
const platform = markdoc?.frontmatter?.platform
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -179,6 +181,7 @@ export default function MyApp({ Component, pageProps }: AppProps<MyAppProps>) {
|
||||
<main className="main-content">
|
||||
<div className="content-wrapper">
|
||||
<div className="article-content mt-5">
|
||||
{markdoc && <PageVersionSwitcher platform={platform} />}
|
||||
<Component {...pageProps} />
|
||||
{markdoc && <PageFooter />}
|
||||
</div>
|
||||
|
||||
@@ -45,28 +45,28 @@ That's it. No configuration needed.
|
||||
|
||||
## Auto Balanced
|
||||
|
||||
`kilo-auto/balanced` follows the same mode-based routing structure as Frontier but uses more cost-effective models — Kimi K2.5 for reasoning-heavy modes and Minimax M2.5 (Free) for implementation modes.
|
||||
`kilo-auto/balanced` follows the same mode-based routing structure as Frontier but uses more cost-effective models — Kimi K2.5 for reasoning-heavy modes and Minimax M2.7 for implementation modes.
|
||||
|
||||
### Mode-to-Model Mapping
|
||||
|
||||
| Mode | Model Used | Best For |
|
||||
| -------------- | ------------------- | ---------------------------- |
|
||||
| `architect` | Kimi K2.5 | System design, planning |
|
||||
| `orchestrator` | Kimi K2.5 | Multi-step task coordination |
|
||||
| `ask` | Kimi K2.5 | Questions, explanations |
|
||||
| `plan` | Kimi K2.5 | Planning, reasoning |
|
||||
| `general` | Kimi K2.5 | General assistance |
|
||||
| `debug` | Kimi K2.5 | Debugging and fixing issues |
|
||||
| `code` | Minimax M2.5 (Free) | Writing and editing code |
|
||||
| `build` | Minimax M2.5 (Free) | Implementation tasks |
|
||||
| `explore` | Minimax M2.5 (Free) | Codebase exploration |
|
||||
| Mode | Model Used | Best For |
|
||||
| -------------- | ------------ | ---------------------------- |
|
||||
| `architect` | Kimi K2.5 | System design, planning |
|
||||
| `orchestrator` | Kimi K2.5 | Multi-step task coordination |
|
||||
| `ask` | Kimi K2.5 | Questions, explanations |
|
||||
| `plan` | Kimi K2.5 | Planning, reasoning |
|
||||
| `general` | Kimi K2.5 | General assistance |
|
||||
| `debug` | Kimi K2.5 | Debugging and fixing issues |
|
||||
| `code` | Minimax M2.7 | Writing and editing code |
|
||||
| `build` | Minimax M2.7 | Implementation tasks |
|
||||
| `explore` | Minimax M2.7 | Codebase exploration |
|
||||
|
||||
**Planning and reasoning tasks** use Kimi K2.5, a strong open-weight reasoning model from Moonshot AI.
|
||||
|
||||
**Implementation tasks** use Minimax M2.5, which provides fast, capable code generation at a fraction of frontier model costs.
|
||||
**Implementation tasks** use Minimax M2.7, which provides fast, capable code generation at a fraction of frontier model costs.
|
||||
|
||||
{% callout type="info" title="Image support" %}
|
||||
Auto Balanced does not support image inputs, since Minimax M2.5 does not have vision capabilities.
|
||||
Auto Balanced does not support image inputs, since Minimax M2.7 does not have vision capabilities.
|
||||
{% /callout %}
|
||||
|
||||
## Benefits
|
||||
|
||||
@@ -82,11 +82,11 @@ Routes to the most capable paid models optimizing for cost, performance, and cap
|
||||
|
||||
Follows the same mode-based routing as Frontier but uses more cost-effective models.
|
||||
|
||||
| Mode | Resolved Model |
|
||||
| -------------------------------------------------------------- | --------------------------- |
|
||||
| `plan`, `general`, `architect`, `orchestrator`, `ask`, `debug` | `moonshotai/kimi-k2.5` |
|
||||
| `build`, `explore`, `code` | `minimax/minimax-m2.5:free` |
|
||||
| Default (no mode specified) | `minimax/minimax-m2.5:free` |
|
||||
| Mode | Resolved Model |
|
||||
| -------------------------------------------------------------- | ---------------------- |
|
||||
| `plan`, `general`, `architect`, `orchestrator`, `ask`, `debug` | `moonshotai/kimi-k2.5` |
|
||||
| `build`, `explore`, `code` | `minimax/minimax-m2.7` |
|
||||
| Default (no mode specified) | `minimax/minimax-m2.7` |
|
||||
|
||||
### `kilo-auto/free`
|
||||
|
||||
|
||||
@@ -20,10 +20,11 @@ Kilo Gateway currently supports BYOK keys for these providers:
|
||||
|
||||
- Anthropic
|
||||
- AWS Bedrock
|
||||
- OpenAI
|
||||
- Google AI Studio
|
||||
- Inception
|
||||
- Minimax
|
||||
- Mistral AI
|
||||
- OpenAI
|
||||
- xAI
|
||||
- Z.AI
|
||||
|
||||
|
||||
@@ -14,54 +14,16 @@ Get started with Kilo Code by installing it on your preferred platform. Choose y
|
||||
|
||||
## VS Code Extension
|
||||
|
||||
{% partial file="install-vscode.md" /%}
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VS Code (Preview)" %}
|
||||
|
||||
## VS Code Preview Extension
|
||||
|
||||
{% callout type="info" %}
|
||||
We're rebuilding Kilo Code from the ground up on the new [Kilo CLI](https://github.com/Kilo-Org/kilocode). The pre-release extension is available for users who want to try the latest architecture and provide feedback, and don't mind some missing features and rough edges.
|
||||
{% /callout %}
|
||||
|
||||
The pre-release extension is a complete rebuild featuring:
|
||||
|
||||
- A new Solid.js-based UI
|
||||
- Deep integration with the Kilo CLI backend
|
||||
- Improved session management and model switching
|
||||
|
||||
### Current Status
|
||||
|
||||
This is an early pre-release. Core features like chat, markdown rendering, authentication, and model/mode switching are working. Some features from the stable extension are still being implemented.
|
||||
|
||||
For the full feature status, see the [feature parity tracking document](https://github.com/Kilo-Org/kilocode/blob/main/packages/kilo-vscode/docs/opencode-migration-plan.md).
|
||||
|
||||
### Installing the Pre-Release
|
||||
The current Kilo Code extension is built on the [Kilo CLI](https://github.com/Kilo-Org/kilocode) and is distributed as the **pre-release version** on the VS Code Marketplace.
|
||||
|
||||
1. Open VS Code
|
||||
2. Go to Extensions (`Ctrl+Shift+X` / `Cmd+Shift+X`)
|
||||
3. Search for "Kilo Code"
|
||||
4. Click the dropdown arrow next to **Install** and select **Install Pre-Release Version**
|
||||
|
||||
### Switching Back to Stable
|
||||
|
||||
If you need to return to the stable version:
|
||||
|
||||
1. Open Extensions in VS Code
|
||||
2. Find Kilo Code
|
||||
3. Click the dropdown and select **Switch to Release Version**
|
||||
|
||||
### Feedback and Issues
|
||||
|
||||
Report issues or provide feedback in the [Kilo-Org/kilocode repository](https://github.com/Kilo-Org/kilocode/issues).
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="JetBrains" %}
|
||||
|
||||
## JetBrains IDEs
|
||||
|
||||
{% partial file="install-jetbrains.md" /%}
|
||||
{% callout type="info" %}
|
||||
The "pre-release" label is a VS Code Marketplace distribution channel — the extension is stable and recommended for all users.
|
||||
{% /callout %}
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
@@ -70,6 +32,27 @@ Report issues or provide feedback in the [Kilo-Org/kilocode repository](https://
|
||||
|
||||
{% partial file="install-cli.md" /%}
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VS Code (Legacy)" %}
|
||||
|
||||
## VS Code Legacy Extension
|
||||
|
||||
The legacy extension is the previous version of Kilo Code for VS Code. It is still available but is no longer actively developed. We recommend installing the current extension (see the **VS Code** tab).
|
||||
|
||||
To install or switch back to the legacy version:
|
||||
|
||||
1. Open VS Code
|
||||
2. Go to Extensions (`Ctrl+Shift+X` / `Cmd+Shift+X`)
|
||||
3. Search for "Kilo Code"
|
||||
4. Click the dropdown arrow next to **Install** and select **Switch to Release Version**
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="JetBrains" %}
|
||||
|
||||
## JetBrains IDEs
|
||||
|
||||
{% partial file="install-jetbrains.md" /%}
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="Slack" %}
|
||||
|
||||
|
||||
@@ -5,12 +5,112 @@ description: "Get up and running with Kilo Code in minutes"
|
||||
|
||||
# Quickstart
|
||||
|
||||
After you [set up Kilo Code](/docs/getting-started/setup-authentication), follow the guide for your platform below.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
## Step by Step Guide
|
||||
|
||||
### Step 1: Open Kilo Code
|
||||
|
||||
Click the Kilo Code icon in the VS Code Primary Side Bar to open the chat panel. If you don't see the icon, verify the [extension is installed](/docs/getting-started/installing).
|
||||
|
||||
### Step 2: Type Your Task
|
||||
|
||||
Type a clear, concise description of what you want Kilo Code to do in the chat box. The same examples work here:
|
||||
|
||||
- "Create a file named `hello.txt` containing 'Hello, world!'."
|
||||
- "Write a Python function that adds two numbers."
|
||||
- "Create an HTML file for a simple website with the title 'Kilo test'"
|
||||
|
||||
No special commands or syntax needed—just use plain English.
|
||||
|
||||
### Step 3: Send Your Task
|
||||
|
||||
Press **Enter** to send.
|
||||
|
||||
### Step 4: Review & Approve Actions
|
||||
|
||||
Kilo Code analyzes your request and proposes actions. By default, most tools are auto-approved — only shell commands, external directory access, and sensitive file reads will prompt for confirmation. You'll see the tool name, arguments, and can approve or reject each action.
|
||||
|
||||
To change which actions require approval, open **Settings** (gear icon) and go to the **Auto-Approve** tab. You can set each tool to Allow, Ask, or Deny. See [Auto-Approving Actions](/docs/getting-started/settings/auto-approving-actions) for details.
|
||||
|
||||
### Step 5: Iterate
|
||||
|
||||
Kilo Code works iteratively. Continue giving feedback or follow-up instructions until your task is complete.
|
||||
|
||||
### Key Differences from Legacy
|
||||
|
||||
- **Settings** are managed via `kilo.jsonc` config files (the Settings webview reads and writes the same files)
|
||||
- **Permissions** use a granular per-tool system instead of broad approval categories
|
||||
- **Modes** are called "agents" and configured as `.md` files or via the `agent` config key
|
||||
- **Autocomplete** uses FIM (Fill-in-the-Middle) with Codestral
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
## CLI Quickstart
|
||||
|
||||
### Step 1: Open a Terminal
|
||||
|
||||
Navigate to your project directory:
|
||||
|
||||
```bash
|
||||
cd /path/to/your/project
|
||||
```
|
||||
|
||||
### Step 2: Launch Kilo
|
||||
|
||||
Run the `kilo` command to start the interactive TUI (terminal user interface):
|
||||
|
||||
```bash
|
||||
kilo
|
||||
```
|
||||
|
||||
If this is your first time, run `kilo auth login` first to authenticate (see [Setup & Authentication](/docs/getting-started/setup-authentication)).
|
||||
|
||||
### Step 3: Type Your Task
|
||||
|
||||
Type your request in natural language at the prompt. The same examples work here:
|
||||
|
||||
- "Create a file named `hello.txt` containing 'Hello, world!'."
|
||||
- "Write a Python function that adds two numbers."
|
||||
- "Create an HTML file for a simple website with the title 'Kilo test'"
|
||||
|
||||
Press **Enter** to send.
|
||||
|
||||
### Step 4: Review & Approve Actions
|
||||
|
||||
Kilo analyzes your request and proposes actions. By default, most tools are auto-approved — only shell commands, external directory access, and sensitive file reads will prompt for confirmation. You'll see the tool name, arguments, and can approve or reject each action.
|
||||
|
||||
To change permission defaults, configure the `permission` key in your `kilo.jsonc` config file. See [Auto-Approving Actions](/docs/getting-started/settings/auto-approving-actions) for details.
|
||||
|
||||
### Step 5: Iterate
|
||||
|
||||
Kilo works iteratively. Continue giving feedback or follow-up instructions until your task is complete.
|
||||
|
||||
### One-Shot Mode
|
||||
|
||||
For quick, non-interactive tasks, use `kilo run`:
|
||||
|
||||
```bash
|
||||
kilo run "add error handling to src/api.ts"
|
||||
```
|
||||
|
||||
Add `--auto` to auto-approve all permissions (use carefully):
|
||||
|
||||
```bash
|
||||
kilo run --auto "fix the failing tests in test/auth.test.ts"
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VSCode (Legacy)" %}
|
||||
|
||||
## Video Tour
|
||||
|
||||
{% youtube url="https://www.youtube.com/watch?v=pO7zRLQS-p0" caption="This quick tour shows how Kilo Code handles a simple request from start to finish" /%}
|
||||
|
||||
After you [set up Kilo Code](/docs/getting-started/setup-authentication), follow these steps:
|
||||
|
||||
## Step by Step Guide
|
||||
|
||||
### Step 1: Open Kilo Code
|
||||
@@ -66,6 +166,9 @@ Kilo Code works iteratively. After each action, it waits for your feedback befor
|
||||
|
||||
{% image src="/docs/img/your-first-task/your-first-task-8.png" alt="Final result of a completed task showing the iteration process" width="500" caption="After completing the task, Kilo Code shows the final result and awaits your next instruction." /%}
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
## Conclusion
|
||||
|
||||
You've completed your first task. Along the way you learned:
|
||||
|
||||
@@ -9,7 +9,243 @@ description: "Configure automatic approval settings for Kilo Code operations"
|
||||
**Security Warning:** Auto-approve settings bypass confirmation prompts, giving Kilo Code direct access to your system. This can result in data loss, file corruption, or worse. Command line access is particularly dangerous, as it can potentially execute harmful operations that could damage your system or compromise security. Only enable auto-approval for actions you fully trust.
|
||||
{% /callout %}
|
||||
|
||||
Auto-approve settings speed up your workflow by eliminating repetitive confirmation prompts, but they significantly increase security risks.
|
||||
Auto-approve settings speed up your workflow by eliminating repetitive confirmation prompts, but they significantly increase security risks. The **VSCode (Legacy)**, **VSCode**, and **CLI** versions each handle permissions differently — choose the tab that matches your setup.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
## Overview
|
||||
|
||||
The extension uses a granular, per-tool permission system. You can configure permissions through the **Settings → Auto Approve** tab, which provides a UI with per-tool **Allow / Ask / Deny** dropdowns.
|
||||
|
||||
The UI reads and writes to the same `kilo.jsonc` config files used by the CLI, so changes made in either place are reflected in both.
|
||||
|
||||
## Permission Levels
|
||||
|
||||
Each tool permission can be set to one of three values:
|
||||
|
||||
| Value | Behavior |
|
||||
| --------- | --------------------------------------------------------- |
|
||||
| `"allow"` | The tool runs automatically without prompting |
|
||||
| `"ask"` | Kilo pauses and asks for approval before running the tool |
|
||||
| `"deny"` | The tool is blocked entirely |
|
||||
|
||||
When no rule matches a permission check, the default action is `ask`.
|
||||
|
||||
## Available Tool Permissions
|
||||
|
||||
The Auto Approve tab lists the following tool-specific permissions. Some tools are grouped together in the UI and share a single permission level:
|
||||
|
||||
| Permission | Controls |
|
||||
| -------------------------- | ------------------------------------------------------ |
|
||||
| `external_directory` | Accessing files outside the project directory |
|
||||
| `bash` | Executing shell commands |
|
||||
| `read` | Reading file contents |
|
||||
| `edit` | Editing existing files |
|
||||
| `glob` | File pattern matching / searching by name |
|
||||
| `grep` | Searching file contents by regex |
|
||||
| `list` | Listing directory contents |
|
||||
| `task` | Launching sub-agents |
|
||||
| `skill` | Loading specialized skills |
|
||||
| `lsp` | Language server protocol operations |
|
||||
| `todoread` / `todowrite` | Reading and updating the todo list |
|
||||
| `websearch` / `codesearch` | Performing web or code searches |
|
||||
| `webfetch` | Fetching content from URLs |
|
||||
| `doom_loop` | Allowing the agent to continue after repeated failures |
|
||||
|
||||
## Runtime Permission Requests
|
||||
|
||||
When a tool is set to `"ask"`, Kilo pauses and displays a permission prompt with two options:
|
||||
|
||||
| Option | Behavior |
|
||||
| -------- | ------------------------------ |
|
||||
| **Run** | Allow this specific invocation |
|
||||
| **Deny** | Block this specific invocation |
|
||||
|
||||
Expand **Manage Auto-Approve Rules** to add commands or patterns to your allowed or denied lists. These rules are then appended to the bottom of the approval rules in settings and the config file.
|
||||
|
||||
## Defaults
|
||||
|
||||
Most tools default to `"*": "allow"` for a smooth out-of-the-box experience. Notable exceptions that prompt by default:
|
||||
|
||||
- **`.env` files** — reading `.env` files prompts for approval. Files matching `*.env.*` (e.g., `.env.local`, `.env.production`) also trigger an ask, while `*.env.example` is explicitly allowed.
|
||||
- **`external_directory`** — accessing files outside the project prompts for approval
|
||||
- **`doom_loop`** — prompts when the agent enters a repeated failure cycle
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
## Overview
|
||||
|
||||
The CLI uses a granular, per-tool permission system configured in `kilo.jsonc`. Instead of broad categories like "read" or "write," each tool has its own permission level with glob-pattern rules for fine-grained control.
|
||||
|
||||
## Permission Levels
|
||||
|
||||
Each tool permission can be set to one of three values:
|
||||
|
||||
| Value | Behavior |
|
||||
| --------- | --------------------------------------------------------- |
|
||||
| `"allow"` | The tool runs automatically without prompting |
|
||||
| `"ask"` | Kilo pauses and asks for approval before running the tool |
|
||||
| `"deny"` | The tool is blocked entirely |
|
||||
|
||||
When no rule matches a permission check, the default action is `ask`.
|
||||
|
||||
## Available Tool Permissions
|
||||
|
||||
Permissions are configured under the `permission` key in `kilo.jsonc`. The following tool-specific permission levels are available:
|
||||
|
||||
| Permission | Controls |
|
||||
| -------------------------- | ------------------------------------------------------ |
|
||||
| `external_directory` | Accessing files outside the project directory |
|
||||
| `bash` | Executing shell commands |
|
||||
| `read` | Reading file contents |
|
||||
| `edit` | Editing existing files |
|
||||
| `glob` | File pattern matching / searching by name |
|
||||
| `grep` | Searching file contents by regex |
|
||||
| `list` | Listing directory contents |
|
||||
| `task` | Launching sub-agents |
|
||||
| `skill` | Loading specialized skills |
|
||||
| `lsp` | Language server protocol operations |
|
||||
| `todoread` / `todowrite` | Reading and updating the todo list |
|
||||
| `websearch` / `codesearch` | Performing web or code searches |
|
||||
| `webfetch` | Fetching content from URLs |
|
||||
| `doom_loop` | Allowing the agent to continue after repeated failures |
|
||||
|
||||
## Glob-Pattern Rules
|
||||
|
||||
Instead of a simple `"allow"` or `"deny"`, each tool can use glob-pattern rules for granular control. Patterns are matched against the tool's arguments (command strings, file paths, etc.), and the last matching rule wins.
|
||||
|
||||
### Example: Shell Commands
|
||||
|
||||
Allow git commands automatically, but prompt for everything else:
|
||||
|
||||
```json
|
||||
{
|
||||
"permission": {
|
||||
"bash": {
|
||||
"git *": "allow",
|
||||
"*": "ask"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example: File Reading
|
||||
|
||||
Prompt before reading `.env` files, but allow all other reads:
|
||||
|
||||
```json
|
||||
{
|
||||
"permission": {
|
||||
"read": {
|
||||
"*.env": "ask",
|
||||
"*": "allow"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Blocking Dangerous Commands
|
||||
|
||||
Deny `rm -rf` commands, allow common dev commands, and ask for anything else:
|
||||
|
||||
```json
|
||||
{
|
||||
"permission": {
|
||||
"bash": {
|
||||
"rm -rf *": "deny",
|
||||
"npm *": "allow",
|
||||
"bun *": "allow",
|
||||
"git *": "allow",
|
||||
"*": "ask"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Per-Agent Permission Overrides
|
||||
|
||||
Different agents can have different permission levels. Override the default permissions for a specific agent under the `agent.<name>.permission` key:
|
||||
|
||||
```json
|
||||
{
|
||||
"permission": {
|
||||
"bash": { "*": "ask" }
|
||||
},
|
||||
"agent": {
|
||||
"code": {
|
||||
"permission": {
|
||||
"bash": { "git *": "allow", "*": "ask" }
|
||||
}
|
||||
},
|
||||
"plan": {
|
||||
"permission": {
|
||||
"bash": { "*": "deny" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the `code` agent can run `git` commands automatically and asks for other shell commands, while the `plan` agent cannot run shell commands at all.
|
||||
|
||||
## Runtime Permission Requests
|
||||
|
||||
When a tool is set to `"ask"`, Kilo pauses and displays a permission prompt. You have three options:
|
||||
|
||||
| Option | Behavior |
|
||||
| ---------------- | -------------------------------------------------------- |
|
||||
| **Allow once** | Allow this specific invocation only |
|
||||
| **Allow always** | Allow this tool (or pattern) for the rest of the session |
|
||||
| **Reject** | Block this specific invocation |
|
||||
|
||||
## Defaults
|
||||
|
||||
Most tools default to `"*": "allow"` for a smooth out-of-the-box experience. Notable exceptions that prompt by default:
|
||||
|
||||
- **`.env` files** — reading `.env` files prompts for approval. Files matching `*.env.*` (e.g., `.env.local`, `.env.production`) also trigger an ask, while `*.env.example` is explicitly allowed.
|
||||
- **`external_directory`** — accessing files outside the project prompts for approval
|
||||
- **`doom_loop`** — prompts when the agent enters a repeated failure cycle
|
||||
|
||||
## Full Configuration Example
|
||||
|
||||
{% callout type="info" %}
|
||||
This is a custom example showing the available configuration options — it does not represent the shipped defaults.
|
||||
{% /callout %}
|
||||
|
||||
```json
|
||||
{
|
||||
"permission": {
|
||||
"read": { "*.env": "ask", "*": "allow" },
|
||||
"edit": { "*.env": "ask", "*": "allow" },
|
||||
"glob": { "*": "allow" },
|
||||
"grep": { "*": "allow" },
|
||||
"list": { "*": "allow" },
|
||||
"bash": { "git *": "allow", "npm *": "allow", "*": "ask" },
|
||||
"task": { "*": "allow" },
|
||||
"skill": { "*": "allow" },
|
||||
"lsp": { "*": "allow" },
|
||||
"todoread": { "*": "allow" },
|
||||
"todowrite": { "*": "allow" },
|
||||
"webfetch": { "*": "allow" },
|
||||
"websearch": { "*": "allow" },
|
||||
"codesearch": { "*": "allow" },
|
||||
"external_directory": { "*": "ask" },
|
||||
"doom_loop": { "*": "ask" }
|
||||
},
|
||||
"agent": {
|
||||
"code": {
|
||||
"permission": {
|
||||
"bash": { "git *": "allow", "npm *": "allow", "*": "ask" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VSCode (Legacy)" %}
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
@@ -343,3 +579,6 @@ You can optionally enable an AI Safety Gatekeeper, which reviews every intended
|
||||
|
||||
This is the fastest way to work with Kilo Code, but also the riskiest. Use it only when you fully trust the AI and are prepared for the consequences.
|
||||
{% /callout %}
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
platform: legacy
|
||||
---
|
||||
|
||||
# Auto Cleanup
|
||||
|
||||
Auto Cleanup automatically manages your task history by removing old tasks to free up disk space and improve performance. Tasks are intelligently classified and retained based on their type and age, ensuring important work is preserved while temporary or experimental tasks are cleaned up.
|
||||
|
||||
@@ -5,13 +5,88 @@ description: "Configure Kilo Code settings and preferences"
|
||||
|
||||
# Settings
|
||||
|
||||
The VS Code extension can be configured through the Settings window, opened by pressing the gear icon. Both the CLI and the extension can also be configured through interactions with the agent. The current VS Code extension and CLI share the same underlying settings, so changes in one are reflected in the other.
|
||||
|
||||
## Managing Settings
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
The VS Code extension provides a **Settings webview UI** accessible from the extension sidebar by clicking the gear icon ({% codicon name="gear" /%}). The UI is organized into tabs including Providers, Auto-Approve, Models, and more.
|
||||
|
||||
This UI reads and writes to the same underlying JSONC config files used by the CLI, so changes made in either place are reflected in both.
|
||||
|
||||
### Config File Locations
|
||||
|
||||
There are two primary config files:
|
||||
|
||||
- **Global config:** `~/.config/kilo/kilo.jsonc` — applies to all projects. On Windows, this is `C:\Users\<username>\.config\kilo\kilo.jsonc`.
|
||||
- **Project config:** `kilo.jsonc` in your project root, or `.kilo/kilo.jsonc` for a cleaner setup. The `.kilo/` version takes priority if both exist.
|
||||
|
||||
{% callout type="warning" %}
|
||||
If you check config files into version control, make sure they do not contain API keys or other secrets (e.g., `provider.*.options.apiKey`). Use environment variables for credentials instead.
|
||||
{% /callout %}
|
||||
|
||||
### Export and Import
|
||||
|
||||
Config files are plain-text and portable — copy them between machines and you're done.
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
In the CLI, settings are managed via **JSONC config files** directly. Config files are plain-text and portable -- you can copy them between machines.
|
||||
|
||||
{% callout type="warning" %}
|
||||
If you check `kilo.jsonc` into version control, make sure it does not contain API keys or other secrets (e.g., `provider.*.options.apiKey`). Use environment variables for credentials instead.
|
||||
{% /callout %}
|
||||
|
||||
### Config File Locations
|
||||
|
||||
There are two primary config files:
|
||||
|
||||
- **Global config:** `~/.config/kilo/kilo.jsonc` -- applies to all projects. On Windows, this is `C:\Users\<username>\.config\kilo\kilo.jsonc`.
|
||||
- **Project config:** `kilo.jsonc` in the root of your project -- overrides global settings for that project.
|
||||
|
||||
Both files use the [JSONC](https://code.visualstudio.com/docs/languages/json#_json-with-comments) format (JSON with comments).
|
||||
|
||||
### Config File Precedence
|
||||
|
||||
Settings are resolved through an 8-level precedence system (lowest to highest priority):
|
||||
|
||||
1. **Legacy Kilocode** -- migrated settings from the VSCode extension
|
||||
2. **Remote well-known** -- remotely fetched defaults
|
||||
3. **Global** -- `~/.config/kilo/kilo.jsonc`
|
||||
4. **Custom** -- additional custom config paths
|
||||
5. **Project** -- `kilo.jsonc` in the project root
|
||||
6. **`.kilo` directory** -- config from a `.kilo/` directory in the project
|
||||
7. **Inline environment** -- environment variable overrides
|
||||
8. **Managed / Enterprise** -- enterprise-managed configuration (highest priority)
|
||||
|
||||
Higher-priority levels override lower ones. This allows organizations to enforce settings at the enterprise level while still letting individual developers customize their local environment.
|
||||
|
||||
### Schema Auto-Injection
|
||||
|
||||
When you create or open a `kilo.jsonc` file, the CLI automatically injects a `$schema` property pointing to the config JSON schema. This gives you **autocompletion and validation** in any editor that supports JSON Schema (VS Code, JetBrains, etc.).
|
||||
|
||||
### Export and Import
|
||||
|
||||
There is no traditional export/import of settings -- the JSONC config files themselves are portable. Copy `~/.config/kilo/kilo.jsonc` or `kilo.jsonc` to another machine and you're done.
|
||||
|
||||
For **session** export and import, use the CLI commands:
|
||||
|
||||
- `kilo export` -- export session data
|
||||
- `kilo import` -- import session data
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VSCode (Legacy)" %}
|
||||
|
||||
Kilo Code allows you to manage your configuration settings effectively through export, import, and reset options. These features are useful for backing up your setup, sharing configurations with others, or restoring default settings if needed.
|
||||
|
||||
You can find these options at the bottom of the Kilo Code settings page, accessible via the gear icon ({% codicon name="gear" /%}) in the Kilo Code chat view.
|
||||
|
||||
{% image src="/docs/img/settings-management/settings-management.png" alt="Export, Import, and Reset buttons in Kilo Code settings" width="800" caption="Export, Import, and Reset buttons" /%}
|
||||
|
||||
## Export Settings
|
||||
### Export Settings
|
||||
|
||||
Clicking the **Export** button saves your current Kilo Code settings to a JSON file.
|
||||
|
||||
@@ -24,7 +99,7 @@ Clicking the **Export** button saves your current Kilo Code settings to a JSON f
|
||||
|
||||
This creates a backup of your configuration or a file you can share.
|
||||
|
||||
## Import Settings
|
||||
### Import Settings
|
||||
|
||||
Clicking the **Import** button allows you to load settings from a previously exported JSON file.
|
||||
|
||||
@@ -35,7 +110,7 @@ Clicking the **Import** button allows you to load settings from a previously exp
|
||||
- **Merging:** Importing settings **merges** the configurations. It adds new API profiles and updates existing ones and global settings based on the file content. It does **not** delete configurations present in your current setup but missing from the imported file.
|
||||
- **Validation:** Only valid settings matching the internal schema can be imported, preventing configuration errors. A success notification appears upon completion.
|
||||
|
||||
## Reset Settings
|
||||
### Reset Settings
|
||||
|
||||
Clicking the **Reset** button completely clears all Kilo Code configuration data and returns the extension to its default state. This is a destructive action intended for troubleshooting or starting fresh.
|
||||
|
||||
@@ -57,8 +132,24 @@ Clicking the **Reset** button completely clears all Kilo Code configuration data
|
||||
|
||||
Use this option only if you are certain you want to remove all Kilo Code data or if instructed during troubleshooting. Consider exporting your settings first if you might want to restore them later.
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
## Experimental Features
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
The extension does not currently expose the same experimental feature toggles as the **VSCode (Legacy)** version. Advanced options are configured via the JSONC config files that the Settings webview reads and writes. Refer to the auto-generated `$schema` in your `kilo.jsonc` for the full list of available options.
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
The CLI does not currently expose the same experimental feature toggles as the **VSCode (Legacy)** version. Configuration of model behavior, file editing strategies, and other advanced options is handled directly in the JSONC config files. Refer to the auto-generated `$schema` in your `kilo.jsonc` for the full list of available options.
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VSCode (Legacy)" %}
|
||||
|
||||
{% callout type="info" %}
|
||||
These features are experimental and may change in future releases. They provide advanced control over Kilo Code's behavior for specific use cases.
|
||||
{% /callout %}
|
||||
@@ -106,3 +197,6 @@ This setting controls the number of lines read from a file in one batch. To mana
|
||||
**Default:** Set in Advanced Settings
|
||||
|
||||
You can find this setting in the Kilo Code settings under 'Advanced Settings'.
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: "System Notifications"
|
||||
description: "Configure native OS notifications for Kilo Code"
|
||||
platform: legacy
|
||||
---
|
||||
|
||||
# System Notifications
|
||||
|
||||
@@ -9,6 +9,31 @@ When you install Kilo Code, you'll be prompted to sign in or create a free accou
|
||||
|
||||
## Quick Start with Kilo Account
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
The extension prompts you to sign in when you first open the sidebar. Click **Sign In** and complete the browser-based flow. The extension communicates with the CLI backend, so authentication is shared between the CLI and extension.
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
Run the auth command and follow the browser-based sign-in flow:
|
||||
|
||||
```bash
|
||||
kilo auth login
|
||||
```
|
||||
|
||||
This may open your browser to complete authentication. Once signed in, your credentials are stored locally and used for all future sessions.
|
||||
|
||||
To verify your auth status:
|
||||
|
||||
```bash
|
||||
kilo auth list
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VSCode (Legacy)" %}
|
||||
|
||||
1. Click **"Try Kilo Code for Free"** in the extension
|
||||
2. Sign in with your Google account
|
||||
3. Allow VS Code to open the authorization URL
|
||||
@@ -17,6 +42,9 @@ When you install Kilo Code, you'll be prompted to sign in or create a free accou
|
||||
|
||||
That's it! You're ready to [start your first task](/docs/getting-started/quickstart).
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
{% callout type="tip" title="Bonus Credits" %}
|
||||
[Add credits to your account](https://app.kilo.ai/profile) and get $20 bonus credits, or sign up for [Kilo Pass](https://kilo.ai/features/kilo-pass).
|
||||
{% /callout %}
|
||||
@@ -75,12 +103,48 @@ Already have a ChatGPT subscription? You can use it with Kilo Code through the [
|
||||
|
||||
### Configuring Your Provider
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
1. Open the Kilo Code sidebar in VS Code
|
||||
2. Click the gear icon ({% codicon name="gear" /%}) to open **Settings**
|
||||
3. Go to the **Providers** tab
|
||||
4. Select your provider and enter your API key
|
||||
5. Choose your model
|
||||
|
||||
You can also use `kilo auth login` for providers that support OAuth (like GitHub Copilot). The extension reads from the same underlying config files as the CLI, so provider settings are shared.
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
Set the API key as an environment variable:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
```
|
||||
|
||||
Or use `kilo auth login` for providers that support OAuth (like GitHub Copilot).
|
||||
|
||||
To set a default model:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"model": "anthropic/claude-sonnet-4-20250514",
|
||||
}
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VSCode (Legacy)" %}
|
||||
|
||||
1. Click the {% kilo-code-icon /%} icon in the VS Code sidebar
|
||||
2. Select your API provider from the dropdown
|
||||
3. Paste your API key
|
||||
4. Choose your model:
|
||||
4. Choose your model
|
||||
5. Click **"Let's go!"**
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
{% callout type="info" title="Need Help?" %}
|
||||
Reach out to our [support team](mailto:hi@kilo.ai) or join our [Discord community](https://kilo.ai/discord).
|
||||
{% /callout %}
|
||||
|
||||
@@ -74,6 +74,10 @@ If the gateway crashes, it's automatically restarted. The machine itself can be
|
||||
Gateway process info is only available when the machine is running.
|
||||
{% /callout %}
|
||||
|
||||
## Instance Specs
|
||||
|
||||
The specs of your instance, including number of CPUs, memory, and storage, are visible at the top right of the instance controls section.
|
||||
|
||||
## Settings
|
||||
|
||||
### Changing the Model
|
||||
|
||||
@@ -77,11 +77,21 @@ You can access instance files in `/root/.openclaw/` directly from the [KiloClaw
|
||||
|
||||
For security reasons, SSH access is currently disabled for all KiloClaw instances. Our primary goal is to provide a secure environment for all users, and restricting direct SSH access is one of the many measures we take to ensure the platform remains safe and protected for everyone.
|
||||
|
||||
### How can I update my OpenClaw?
|
||||
### Can I install tailscale on my KiloClaw instance?
|
||||
|
||||
Do **not** click **Update Now** inside the OpenClaw Control UI — this is not supported for KiloClaw instances and may break your setup.
|
||||
Not at this time.
|
||||
|
||||
Updates are managed by the KiloClaw platform team to ensure stability. When a new version is available, it will be announced in the **Changelog** on your dashboard. To apply the update, click **Upgrade & Redeploy** from the [KiloClaw Dashboard](/docs/kiloclaw/dashboard#redeploy).
|
||||
### Can I update the version of Node on my instance?
|
||||
|
||||
Not at this time.
|
||||
|
||||
### How do I upgrade the OpenClaw version? Can I upgrade it myself?
|
||||
|
||||
Updates are managed by the KiloClaw platform team to ensure stability and dependency availability. When a new version is available, it will be announced in the **Changelog** on your dashboard.
|
||||
|
||||
To apply the update, click **Upgrade & Redeploy** from the [KiloClaw Dashboard](https://app.kilo.ai/claw). Do **not** click **Update Now** inside the OpenClaw Control UI — this is not supported for KiloClaw instances and may break your setup.
|
||||
|
||||
If you need to roll back, use the [version pinning](/docs/kiloclaw/version-pinning) dialog in the Settings tab.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# Source Code Links
|
||||
|
||||
<!-- Auto-generated by script/extract-source-links.ts — DO NOT EDIT -->
|
||||
<!-- 75 unique URLs extracted from extension and CLI source -->
|
||||
<!-- 76 unique URLs extracted from extension and CLI source -->
|
||||
|
||||
- <https://api.kilo.ai>
|
||||
<!-- packages/opencode/src/cli/cmd/github.ts -->
|
||||
- <https://app.kilo.ai>
|
||||
<!-- packages/opencode/src/kilocode/kilo-commands.tsx -->
|
||||
<!-- packages/opencode/src/session/retry.ts -->
|
||||
- <https://app.kilo.ai/config.json>
|
||||
<!-- packages/opencode/src/config/config.ts -->
|
||||
- <https://app.kilo.ai/credits>
|
||||
<!-- packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts -->
|
||||
- <https://app.kilo.ai/profile>
|
||||
<!-- packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx -->
|
||||
- <https://app.kilo.ai/usage>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||
@@ -29,6 +29,9 @@
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@ai-sdk/anthropic": "2.0.65",
|
||||
"@ai-sdk/openai": "2.0.101",
|
||||
"@ai-sdk/openai-compatible": "1.0.32",
|
||||
"@openrouter/ai-sdk-provider": "1.5.4",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"ai": "catalog:",
|
||||
|
||||
@@ -69,3 +69,7 @@ export const HEADER_FEATURE = "X-KILOCODE-FEATURE"
|
||||
|
||||
/** Environment variable name for feature override */
|
||||
export const ENV_FEATURE = "KILOCODE_FEATURE"
|
||||
|
||||
export const PROMPTS = ["codex", "gemini", "beast", "anthropic", "trinity", "anthropic_without_todo"] as const
|
||||
|
||||
export const AI_SDK_PROVIDERS = ["anthropic", "openai", "openai-compatible", "openrouter"] as const
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod"
|
||||
import { getKiloUrlFromToken } from "../auth/token.js"
|
||||
import { DEFAULT_HEADERS } from "../headers.js"
|
||||
import { KILO_API_BASE, KILO_OPENROUTER_BASE, MODELS_FETCH_TIMEOUT_MS } from "./constants.js"
|
||||
import { KILO_API_BASE, KILO_OPENROUTER_BASE, MODELS_FETCH_TIMEOUT_MS, PROMPTS, AI_SDK_PROVIDERS } from "./constants.js"
|
||||
|
||||
/**
|
||||
* OpenRouter model schema
|
||||
@@ -34,11 +34,9 @@ const openRouterModelSchema = z.object({
|
||||
opencode: z
|
||||
.object({
|
||||
family: z.string().optional(),
|
||||
prompt: z
|
||||
.enum(["codex", "gemini", "beast", "anthropic", "trinity", "anthropic_without_todo"])
|
||||
.optional()
|
||||
.catch(undefined),
|
||||
prompt: z.enum(PROMPTS).optional().catch(undefined),
|
||||
variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
|
||||
ai_sdk_provider: z.enum(AI_SDK_PROVIDERS).optional().catch(undefined),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
@@ -50,12 +48,15 @@ const openRouterModelsResponseSchema = z.object({
|
||||
type OpenRouterModel = z.infer<typeof openRouterModelSchema>
|
||||
|
||||
/**
|
||||
* Parse API price string to number (e.g. "0.00001" -> 0.00001)
|
||||
* Parse API price string to number, converting from per-token to per-million-tokens.
|
||||
* The API returns prices in $/token, but downstream cost calculation (getUsage)
|
||||
* divides by 1,000,000 expecting $/M tokens.
|
||||
*/
|
||||
function parseApiPrice(price: string | null | undefined): number | undefined {
|
||||
if (!price) return undefined
|
||||
const parsed = parseFloat(price)
|
||||
return isNaN(parsed) ? undefined : parsed
|
||||
if (isNaN(parsed)) return undefined
|
||||
return parsed * 1_000_000 // Convert $/token → $/M tokens
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,6 +163,7 @@ function transformToModelDevFormat(model: OpenRouterModel): any {
|
||||
recommendedIndex: model.preferredIndex,
|
||||
variants: model.opencode?.variants,
|
||||
prompt: model.opencode?.prompt,
|
||||
ai_sdk_provider: model.opencode?.ai_sdk_provider,
|
||||
tool_call: supportsTools,
|
||||
isFree: model.isFree,
|
||||
...(inputPrice !== undefined &&
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { z } from "zod"
|
||||
import { KILO_API_BASE, MODELS_FETCH_TIMEOUT_MS } from "./constants.js"
|
||||
import { DEFAULT_HEADERS } from "../headers.js"
|
||||
|
||||
/**
|
||||
* Group entry in an organization mode config.
|
||||
* Either a simple group name or a tuple for edit with file restrictions.
|
||||
*/
|
||||
const EditGroupConfigSchema = z.object({
|
||||
fileRegex: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
const GroupEntrySchema = z.union([z.string(), z.tuple([z.string(), EditGroupConfigSchema])])
|
||||
|
||||
const OrganizationModeConfigSchema = z.object({
|
||||
roleDefinition: z.string().optional(),
|
||||
whenToUse: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
customInstructions: z.string().optional(),
|
||||
groups: z.array(GroupEntrySchema).optional(),
|
||||
})
|
||||
|
||||
const OrganizationModeSchema = z.object({
|
||||
id: z.string(),
|
||||
organization_id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
created_by: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
config: OrganizationModeConfigSchema,
|
||||
})
|
||||
|
||||
const ResponseSchema = z.object({
|
||||
modes: z.array(OrganizationModeSchema),
|
||||
})
|
||||
|
||||
export type OrganizationModeConfig = z.infer<typeof OrganizationModeConfigSchema>
|
||||
export type OrganizationMode = z.infer<typeof OrganizationModeSchema>
|
||||
|
||||
/**
|
||||
* In-memory cache for organization modes, keyed by organizationId.
|
||||
*/
|
||||
const cache = new Map<string, { modes: OrganizationMode[]; timestamp: number }>()
|
||||
const TTL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
/**
|
||||
* Clear the organization modes cache.
|
||||
* Should be called when switching organizations.
|
||||
*/
|
||||
export function clearModesCache() {
|
||||
cache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch custom modes for an organization from the Kilo Cloud API.
|
||||
*
|
||||
* @param token - Bearer authentication token
|
||||
* @param organizationId - Organization UUID
|
||||
* @returns Array of organization modes, or empty array on error
|
||||
*/
|
||||
export async function fetchOrganizationModes(token: string, organizationId: string): Promise<OrganizationMode[]> {
|
||||
const cached = cache.get(organizationId)
|
||||
if (cached && Date.now() - cached.timestamp < TTL) {
|
||||
return cached.modes
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `${KILO_API_BASE}/api/organizations/${encodeURIComponent(organizationId)}/modes`
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`[Kilo Gateway] Failed to fetch organization modes: ${response.status}`)
|
||||
return []
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
const parsed = ResponseSchema.safeParse(json)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.warn("[Kilo Gateway] Organization modes response validation failed:", parsed.error.format())
|
||||
return []
|
||||
}
|
||||
|
||||
const modes = parsed.data.modes
|
||||
cache.set(organizationId, { modes, timestamp: Date.now() })
|
||||
return modes
|
||||
} catch (err) {
|
||||
console.warn("[Kilo Gateway] Error fetching organization modes:", err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,12 @@ export {
|
||||
promptOrganizationSelection,
|
||||
} from "./api/profile.js"
|
||||
export { fetchKiloModels } from "./api/models.js"
|
||||
export {
|
||||
fetchOrganizationModes,
|
||||
clearModesCache,
|
||||
type OrganizationMode,
|
||||
type OrganizationModeConfig,
|
||||
} from "./api/modes.js"
|
||||
export { fetchKilocodeNotifications, type KilocodeNotification } from "./api/notifications.js"
|
||||
|
||||
// ============================================================================
|
||||
@@ -60,6 +66,7 @@ export type {
|
||||
PollOptions,
|
||||
PollResult,
|
||||
// Provider types
|
||||
KiloProvider,
|
||||
KiloProviderOptions,
|
||||
KiloMetadata,
|
||||
CustomLoaderResult,
|
||||
@@ -96,4 +103,6 @@ export {
|
||||
ENV_VERSION,
|
||||
TESTER_SUPPRESS_VALUE,
|
||||
ENV_FEATURE,
|
||||
PROMPTS,
|
||||
AI_SDK_PROVIDERS,
|
||||
} from "./api/constants.js"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
||||
import type { Provider as SDK } from "ai"
|
||||
import type { KiloProviderOptions } from "./types.js"
|
||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
||||
import type { KiloProvider, KiloProviderOptions } from "./types.js"
|
||||
import { getKiloUrlFromToken, getApiKey } from "./auth/token.js"
|
||||
import { buildKiloHeaders, DEFAULT_HEADERS } from "./headers.js"
|
||||
import { KILO_API_BASE, ANONYMOUS_API_KEY } from "./api/constants.js"
|
||||
@@ -21,7 +23,7 @@ import { KILO_API_BASE, ANONYMOUS_API_KEY } from "./api/constants.js"
|
||||
* const model = provider.languageModel("anthropic/claude-sonnet-4")
|
||||
* ```
|
||||
*/
|
||||
export function createKilo(options: KiloProviderOptions = {}): SDK {
|
||||
export function createKilo(options: KiloProviderOptions = {}): KiloProvider {
|
||||
// Get API key from options or environment
|
||||
const apiKey = getApiKey(options)
|
||||
|
||||
@@ -66,11 +68,36 @@ export function createKilo(options: KiloProviderOptions = {}): SDK {
|
||||
})
|
||||
}
|
||||
|
||||
// Create OpenRouter provider with KiloCode configuration
|
||||
return createOpenRouter({
|
||||
const sdkOptions = {
|
||||
baseURL: openRouterUrl,
|
||||
apiKey: apiKey ?? ANONYMOUS_API_KEY,
|
||||
headers: customHeaders,
|
||||
fetch: wrappedFetch as typeof fetch,
|
||||
})
|
||||
}
|
||||
|
||||
const openrouter = createOpenRouter(sdkOptions)
|
||||
const anthropic = createAnthropic(sdkOptions)
|
||||
const openai = createOpenAI(sdkOptions)
|
||||
const openaiCompatible = createOpenAICompatible({ ...sdkOptions, name: "openaiCompatible" })
|
||||
|
||||
return {
|
||||
languageModel(modelId) {
|
||||
return openrouter(modelId)
|
||||
},
|
||||
textEmbeddingModel(modelId) {
|
||||
return openrouter.textEmbeddingModel(modelId)
|
||||
},
|
||||
imageModel(modelId) {
|
||||
return openrouter.imageModel(modelId)
|
||||
},
|
||||
anthropic(modelId) {
|
||||
return anthropic(modelId)
|
||||
},
|
||||
openai(modelId) {
|
||||
return openai(modelId)
|
||||
},
|
||||
openaiCompatible(modelId) {
|
||||
return openaiCompatible(modelId)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { fetchProfile, fetchBalance } from "../api/profile.js"
|
||||
import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js"
|
||||
import { fetchOrganizationModes, clearModesCache } from "../api/modes.js"
|
||||
import { KILO_API_BASE, HEADER_FEATURE } from "../api/constants.js"
|
||||
import { buildKiloHeaders } from "../headers.js"
|
||||
import type { ImportDeps, DrizzleDb } from "../cloud-sessions.js"
|
||||
@@ -204,10 +205,84 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
|
||||
})
|
||||
|
||||
ModelCache.clear("kilo")
|
||||
clearModesCache()
|
||||
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/modes",
|
||||
describeRoute({
|
||||
summary: "Get organization custom modes",
|
||||
description: "Fetch custom modes defined for the current organization",
|
||||
operationId: "kilo.modes",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Organization modes list",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.object({
|
||||
modes: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
organization_id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
created_by: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
config: z.object({
|
||||
roleDefinition: z.string().optional(),
|
||||
whenToUse: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
customInstructions: z.string().optional(),
|
||||
groups: z
|
||||
.array(
|
||||
z.union([
|
||||
z.string(),
|
||||
z.tuple([
|
||||
z.string(),
|
||||
z.object({ fileRegex: z.string().optional(), description: z.string().optional() }),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c: any) => {
|
||||
const auth = await Auth.get("kilo")
|
||||
|
||||
if (!auth || auth.type !== "oauth") {
|
||||
return c.json({ modes: [] })
|
||||
}
|
||||
|
||||
const token = auth.access
|
||||
if (!token) {
|
||||
return c.json({ modes: [] })
|
||||
}
|
||||
|
||||
const orgId = auth.accountId
|
||||
if (!orgId) {
|
||||
return c.json({ modes: [] })
|
||||
}
|
||||
|
||||
try {
|
||||
const modes = await fetchOrganizationModes(token, orgId)
|
||||
return c.json({ modes })
|
||||
} catch {
|
||||
return c.json({ modes: [] })
|
||||
}
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/fim",
|
||||
describeRoute({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Provider as SDK } from "ai"
|
||||
import type { LanguageModel, Provider, Provider as SDK } from "ai"
|
||||
import type { LanguageModelV2 } from "@openrouter/ai-sdk-provider"
|
||||
|
||||
// ============================================================================
|
||||
@@ -159,5 +159,11 @@ export interface ProviderInfo {
|
||||
models: Record<string, any>
|
||||
}
|
||||
|
||||
export type KiloProvider = Provider & {
|
||||
anthropic(modelId: string): LanguageModel
|
||||
openai(modelId: string): LanguageModel
|
||||
openaiCompatible(modelId: string): LanguageModel
|
||||
}
|
||||
|
||||
// Re-export LanguageModelV2 for convenience
|
||||
export type { LanguageModelV2 }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Kilo-specific i18n translations and overrides",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.8",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -150,15 +150,15 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
/* User message copy button: tiny reserved space, button overflows on hover.
|
||||
* height:2px keeps layout stable; overflow:hidden clips the invisible button;
|
||||
* on hover overflow:visible lets the button appear without shifting content. */
|
||||
/* User message copy button: collapse wrapper when idle, show on hover.
|
||||
* The wrapper must be tall enough to fully contain the button (20px)
|
||||
* so that hovering the button keeps [data-component="user-message"]
|
||||
* hovered — otherwise :hover is lost and pointer-events/opacity revert. */
|
||||
[data-component="user-message"] {
|
||||
[data-slot="user-message-copy-wrapper"] {
|
||||
min-height: 0;
|
||||
height: 10px;
|
||||
height: 20px;
|
||||
margin-top: 0;
|
||||
overflow: hidden;
|
||||
|
||||
[data-slot="user-message-meta-wrap"] {
|
||||
display: none;
|
||||
@@ -169,11 +169,6 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] {
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover [data-slot="user-message-copy-wrapper"],
|
||||
&:focus-within [data-slot="user-message-copy-wrapper"] {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="question-answers"] {
|
||||
@@ -291,3 +286,26 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="tool-hint"] {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border-left: 2px solid var(--border-weak-base);
|
||||
|
||||
[data-component="icon"] {
|
||||
flex-shrink: 0;
|
||||
color: var(--icon-weak-base);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
[data-slot="tool-hint-message"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: italic;
|
||||
color: var(--text-weak);
|
||||
opacity: 0.9;
|
||||
line-height: var(--line-height-large);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,6 +1077,20 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const hint =
|
||||
cleaned.includes("before overwriting it. Use the Read tool first") ||
|
||||
cleaned.includes("has been modified since it was last read") ||
|
||||
cleaned.includes("oldString and newString are identical") ||
|
||||
cleaned.includes("must match exactly, including whitespace") ||
|
||||
cleaned.includes("Found multiple matches for oldString")
|
||||
if (hint) {
|
||||
return (
|
||||
<div data-component="tool-hint">
|
||||
<Icon name="arrow-right" size="small" />
|
||||
<span data-slot="tool-hint-message">{cleaned}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const [title, ...rest] = cleaned.split(": ")
|
||||
return (
|
||||
<Card variant="error">
|
||||
@@ -1292,6 +1306,15 @@ function useToolReveal(pending: () => boolean, animate?: () => boolean) {
|
||||
function WebfetchMeta(props: { url: string; animate?: boolean }) {
|
||||
let ref: HTMLSpanElement | undefined
|
||||
useToolFade(() => ref, { wipe: true, animate: props.animate })
|
||||
const data = useData()
|
||||
|
||||
const open = (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
const handler = data.openUrl
|
||||
if (handler) return handler(props.url)
|
||||
window.open(props.url, "_blank", "noopener,noreferrer")
|
||||
}
|
||||
|
||||
return (
|
||||
<span ref={ref} data-slot="webfetch-meta">
|
||||
@@ -1302,11 +1325,11 @@ function WebfetchMeta(props: { url: string; animate?: boolean }) {
|
||||
href={props.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onClick={open}
|
||||
>
|
||||
{props.url}
|
||||
</a>
|
||||
<div data-component="tool-action">
|
||||
<div data-component="tool-action" onClick={open} style={{ cursor: "pointer" }}>
|
||||
<Icon name="square-arrow-top-right" size="small" />
|
||||
</div>
|
||||
</span>
|
||||
@@ -1993,8 +2016,8 @@ interface ApplyPatchFile {
|
||||
relativePath: string
|
||||
type: "add" | "update" | "delete" | "move"
|
||||
diff: string
|
||||
before: string
|
||||
after: string
|
||||
before?: string
|
||||
after?: string
|
||||
additions: number
|
||||
deletions: number
|
||||
movePath?: string
|
||||
@@ -2049,7 +2072,6 @@ ToolRegistry.register({
|
||||
path={file().relativePath.includes("/") ? getDirectory(file().relativePath) : undefined}
|
||||
changes={{ additions: file().additions, deletions: file().deletions }}
|
||||
animate={reveal()}
|
||||
soft
|
||||
onClick={
|
||||
data.openFile && file().filePath
|
||||
? (e: MouseEvent) => {
|
||||
@@ -2145,7 +2167,7 @@ ToolRegistry.register({
|
||||
</Accordion.Trigger>
|
||||
</StickyAccordionHeader>
|
||||
<Accordion.Content>
|
||||
<Show when={visible()}>
|
||||
<Show when={visible() && file.before !== undefined}>
|
||||
<div data-component="apply-patch-file-diff">
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
@@ -2193,14 +2215,16 @@ ToolRegistry.register({
|
||||
</Switch>
|
||||
}
|
||||
>
|
||||
<div data-component="apply-patch-file-diff">
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
before={{ name: file().filePath, contents: file().before }}
|
||||
after={{ name: file().movePath ?? file().filePath, contents: file().after }}
|
||||
/>
|
||||
</div>
|
||||
<Show when={file().before !== undefined}>
|
||||
<div data-component="apply-patch-file-diff">
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
before={{ name: file().filePath, contents: file().before }}
|
||||
after={{ name: file().movePath ?? file().filePath, contents: file().after }}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</ToolFileAccordion>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from "@opencode-ai/ui/text-shimmer"
|
||||
export * from "@opencode-ai/ui/text-shimmer"
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from "@opencode-ai/ui/tool-count-summary"
|
||||
export * from "@opencode-ai/ui/tool-count-summary"
|
||||
|
||||
@@ -7,7 +7,6 @@ const DEBOUNCE_MS = 100
|
||||
export interface AutoScrollOptions {
|
||||
working: () => boolean
|
||||
onUserInteracted?: () => void
|
||||
overflowAnchor?: "none" | "auto" | "dynamic"
|
||||
bottomThreshold?: number
|
||||
}
|
||||
|
||||
@@ -15,10 +14,10 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
let scroll: HTMLElement | undefined
|
||||
let settling = false
|
||||
let settleTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let autoTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let stopTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let cleanup: (() => void) | undefined
|
||||
let auto: { time: number } | undefined
|
||||
let userInitiated = false
|
||||
let lastScrollTop: number | undefined
|
||||
|
||||
const threshold = () => options.bottomThreshold ?? 10
|
||||
|
||||
@@ -37,36 +36,18 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
return el.scrollHeight - el.clientHeight > 1
|
||||
}
|
||||
|
||||
// Browsers can dispatch scroll events asynchronously. If new content arrives
|
||||
// between us calling `scrollTo()` and the subsequent `scroll` event firing,
|
||||
// the handler can see a non-zero `distanceFromBottom` and incorrectly assume
|
||||
// the user scrolled.
|
||||
const markAuto = (_el: HTMLElement) => {
|
||||
auto = { time: Date.now() }
|
||||
|
||||
if (autoTimer) clearTimeout(autoTimer)
|
||||
autoTimer = setTimeout(() => {
|
||||
auto = undefined
|
||||
autoTimer = undefined
|
||||
}, 250)
|
||||
}
|
||||
|
||||
const isAuto = (_el: HTMLElement) => {
|
||||
const a = auto
|
||||
if (!a) return false
|
||||
|
||||
if (Date.now() - a.time > 250) {
|
||||
auto = undefined
|
||||
return false
|
||||
const markUser = (e: Event) => {
|
||||
if (e instanceof WheelEvent) {
|
||||
const target = e.target instanceof Element ? e.target : undefined
|
||||
const nested = target?.closest("[data-scrollable]")
|
||||
if (scroll && nested && nested !== scroll) return
|
||||
}
|
||||
|
||||
return true
|
||||
userInitiated = true
|
||||
}
|
||||
|
||||
const scrollToBottomNow = (behavior: ScrollBehavior) => {
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
markAuto(el)
|
||||
if (behavior === "smooth") {
|
||||
el.scrollTo({ top: el.scrollHeight, behavior })
|
||||
return
|
||||
@@ -74,6 +55,7 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
|
||||
// `scrollTop` assignment bypasses any CSS `scroll-behavior: smooth`.
|
||||
el.scrollTop = el.scrollHeight
|
||||
lastScrollTop = el.scrollTop
|
||||
}
|
||||
|
||||
const scrollToBottom = (force: boolean) => {
|
||||
@@ -121,19 +103,28 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
|
||||
const byUser = userInitiated
|
||||
userInitiated = false
|
||||
const distance = distanceFromBottom(el)
|
||||
|
||||
if (!canScroll(el)) {
|
||||
if (store.userScrolled) setStore("userScrolled", false)
|
||||
return
|
||||
}
|
||||
|
||||
if (distanceFromBottom(el) < threshold()) {
|
||||
if (distance < threshold()) {
|
||||
if (store.userScrolled) setStore("userScrolled", false)
|
||||
lastScrollTop = el.scrollTop
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore scroll events triggered by our own scrollToBottom calls.
|
||||
if (!store.userScrolled && isAuto(el)) {
|
||||
scrollToBottom(false)
|
||||
if (!store.userScrolled && !byUser) {
|
||||
if (el.scrollTop < (lastScrollTop ?? el.scrollTop)) {
|
||||
stop()
|
||||
} else {
|
||||
scrollToBottomNow("auto")
|
||||
}
|
||||
lastScrollTop = el.scrollTop
|
||||
return
|
||||
}
|
||||
|
||||
@@ -145,7 +136,6 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
const cur = scroll
|
||||
if (!cur) return
|
||||
if (distanceFromBottom(cur) < threshold()) return
|
||||
if (!store.userScrolled && isAuto(cur)) return
|
||||
stop()
|
||||
}, DEBOUNCE_MS)
|
||||
}
|
||||
@@ -155,22 +145,6 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
stop()
|
||||
}
|
||||
|
||||
const updateOverflowAnchor = (el: HTMLElement) => {
|
||||
const mode = options.overflowAnchor ?? "dynamic"
|
||||
|
||||
if (mode === "none") {
|
||||
el.style.overflowAnchor = "none"
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === "auto") {
|
||||
el.style.overflowAnchor = "auto"
|
||||
return
|
||||
}
|
||||
|
||||
el.style.overflowAnchor = store.userScrolled ? "auto" : "none"
|
||||
}
|
||||
|
||||
createResizeObserver(
|
||||
() => store.contentRef,
|
||||
() => {
|
||||
@@ -179,8 +153,16 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
if (store.userScrolled) setStore("userScrolled", false)
|
||||
return
|
||||
}
|
||||
if (!active()) return
|
||||
if (store.userScrolled) return
|
||||
if (!active()) {
|
||||
if (!store.userScrolled && el && distanceFromBottom(el) > threshold()) {
|
||||
scrollToBottomNow("auto")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if (store.userScrolled) {
|
||||
return
|
||||
}
|
||||
// ResizeObserver fires after layout, before paint.
|
||||
// Keep the bottom locked in the same frame to avoid visible
|
||||
// "jump up then catch up" artifacts while streaming content.
|
||||
@@ -206,18 +188,8 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
}),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
// Track `userScrolled` even before `scrollRef` is attached, so we can
|
||||
// update overflow anchoring once the element exists.
|
||||
store.userScrolled
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
updateOverflowAnchor(el)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (settleTimer) clearTimeout(settleTimer)
|
||||
if (autoTimer) clearTimeout(autoTimer)
|
||||
if (stopTimer) clearTimeout(stopTimer)
|
||||
if (cleanup) cleanup()
|
||||
})
|
||||
@@ -233,11 +205,19 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
|
||||
if (!el) return
|
||||
|
||||
updateOverflowAnchor(el)
|
||||
el.style.overflowAnchor = "auto"
|
||||
el.addEventListener("wheel", handleWheel, { passive: true })
|
||||
el.addEventListener("wheel", markUser, { passive: true, capture: true })
|
||||
el.addEventListener("pointerdown", markUser, { passive: true })
|
||||
el.addEventListener("keydown", markUser, { passive: true })
|
||||
el.addEventListener("touchstart", markUser, { passive: true })
|
||||
|
||||
cleanup = () => {
|
||||
el.removeEventListener("wheel", handleWheel)
|
||||
el.removeEventListener("wheel", markUser, { capture: true })
|
||||
el.removeEventListener("pointerdown", markUser)
|
||||
el.removeEventListener("keydown", markUser)
|
||||
el.removeEventListener("touchstart", markUser)
|
||||
}
|
||||
},
|
||||
contentRef: (el: HTMLElement | undefined) => setStore("contentRef", el),
|
||||
|
||||
@@ -40,12 +40,7 @@ export const NoChildren: Story = {
|
||||
|
||||
export const Locked: Story = {
|
||||
render: () => (
|
||||
<BasicTool
|
||||
icon="folder"
|
||||
trigger={{ title: "Writing file", subtitle: "output.txt" }}
|
||||
defaultOpen
|
||||
locked
|
||||
>
|
||||
<BasicTool icon="folder" trigger={{ title: "Writing file", subtitle: "output.txt" }} defaultOpen locked>
|
||||
<pre style={{ padding: "8px", margin: 0, "font-size": "12px" }}>{"Hello, world!"}</pre>
|
||||
</BasicTool>
|
||||
),
|
||||
|
||||
@@ -353,3 +353,91 @@ export const WithContextGroup: Story = {
|
||||
</AllProviders>
|
||||
),
|
||||
}
|
||||
|
||||
// --- All 5 tool hint error types in a single screenshot ---
|
||||
|
||||
const hintErrors: ToolPart[] = [
|
||||
{
|
||||
id: "part-hint-001",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-hint-001",
|
||||
tool: "write",
|
||||
state: {
|
||||
status: "error",
|
||||
input: { filePath: "src/config.ts" },
|
||||
error:
|
||||
"Error: File already exists — read it before overwriting it. Use the Read tool first to see its current contents.",
|
||||
time: { start: now - 6000, end: now - 5500 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "part-hint-002",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-hint-002",
|
||||
tool: "edit",
|
||||
state: {
|
||||
status: "error",
|
||||
input: { filePath: "src/app.tsx" },
|
||||
error: "Error: File has been modified since it was last read. Please read it again before editing.",
|
||||
time: { start: now - 5500, end: now - 5000 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "part-hint-003",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-hint-003",
|
||||
tool: "edit",
|
||||
state: {
|
||||
status: "error",
|
||||
input: { filePath: "src/utils.ts" },
|
||||
error: "Error: oldString and newString are identical. No changes were made.",
|
||||
time: { start: now - 5000, end: now - 4500 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "part-hint-004",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-hint-004",
|
||||
tool: "edit",
|
||||
state: {
|
||||
status: "error",
|
||||
input: { filePath: "src/index.ts" },
|
||||
error:
|
||||
"Error: oldString not found in file. The oldString must match exactly, including whitespace and indentation.",
|
||||
time: { start: now - 4500, end: now - 4000 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "part-hint-005",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-hint-005",
|
||||
tool: "edit",
|
||||
state: {
|
||||
status: "error",
|
||||
input: { filePath: "src/counter.tsx" },
|
||||
error:
|
||||
"Error: Found multiple matches for oldString. Provide more surrounding lines to identify the correct match.",
|
||||
time: { start: now - 4000, end: now - 3500 },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const mockDataHintErrors = createMockData(hintErrors)
|
||||
|
||||
export const ToolHintErrors: Story = {
|
||||
render: () => (
|
||||
<AllProviders data={mockDataHintErrors}>
|
||||
<AssistantParts messages={[mockAssistantMessage]} />
|
||||
</AllProviders>
|
||||
),
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9cad4954b9ac5c0d1a245d14b3ab51836217479870c39589a55eb862c4247aa9
|
||||
size 18902
|
||||
oid sha256:81fd232992e804e8e0b3453c4e77d358f8c24dd7495b354b8d7de24c7a43724c
|
||||
size 18986
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:958b3b690c8e19c8c4ffc451da476954e9334f5619a1b8b40ce30da848240f6d
|
||||
size 15354
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:876fba84b341bffa55bedb3c6d3384c5099c9a1d9336a3090b429b87e1256110
|
||||
size 4149
|
||||
oid sha256:4b690e1cba9e1a7a4eef86162c762092f5908f2d498f0d77f48c44fef1fd44ff
|
||||
size 4242
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0fc349b25120ba40eaf3f6e6cb9adf268a0433c6a4b4e723f5a9957c555aec25
|
||||
size 29267
|
||||
oid sha256:71a4c5ce0d2bd00ac8428a65e3c4db6c0455720cac5467f8f483a9708cea8765
|
||||
size 29370
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c67bd29525bc38133cc5271d0467e85e131a0bd41313a4033836e7a6bc5e02e2
|
||||
size 26312
|
||||
oid sha256:71a4c5ce0d2bd00ac8428a65e3c4db6c0455720cac5467f8f483a9708cea8765
|
||||
size 29370
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:640fabcec21e9e8e2251e1b347966790d347af9c0b438ca4dc9bc118819069f2
|
||||
size 4562
|
||||
oid sha256:4272e7e538d95ee3c7010dca5711a53643573c0efed2c527d6db58ef23d6c086
|
||||
size 4627
|
||||
|
||||
@@ -201,6 +201,16 @@ New webview features must use **`@kilocode/kilo-ui`** components instead of raw
|
||||
|
||||
This package is entirely Kilo-specific — `kilocode_change` markers are NOT needed in any files under `packages/kilo-vscode/`. The markers are only necessary when modifying shared upstream opencode files.
|
||||
|
||||
## Process Spawning (Windows)
|
||||
|
||||
On Windows, any `spawn`/`execFile`/`exec` call that does not set `windowsHide: true` will flash a cmd.exe console window at the user. To prevent this, **never import `spawn`, `execFile`, or `exec` from `child_process` directly**. Use the wrappers in `src/util/process.ts` instead — they enforce `windowsHide: true` automatically:
|
||||
|
||||
```ts
|
||||
import { spawn, exec } from "../util/process"
|
||||
```
|
||||
|
||||
The `spawn` wrapper covers long-lived processes (e.g. `kilo serve`). The `exec` wrapper covers short commands (e.g. `git`, `tar`). If you need the raw callback form of `execFile` for some reason, pass `windowsHide: true` explicitly in the options object.
|
||||
|
||||
## Style
|
||||
|
||||
Follow monorepo root AGENTS.md style guide:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Kilo Code
|
||||
Copyright (c) 2025 opencode
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,9 +1,9 @@
|
||||
<p align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
|
||||
<a href="https://x.com/kilocode"><img src="https://img.shields.io/badge/kilocode-000000?style=flat&logo=x&logoColor=white" alt="X (Twitter)"></a>
|
||||
<a href="https://blog.kilo.ai"><img src="https://img.shields.io/badge/Blog-555?style=flat&logo=substack&logoColor=white" alt="Substack Blog"></a>
|
||||
<a href="https://kilo.ai/discord"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
|
||||
<a href="https://www.reddit.com/r/kilocode/"><img src="https://img.shields.io/badge/Join%20r%2Fkilocode-D84315?style=flat&logo=reddit&logoColor=white" alt="Reddit"></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code"><img src="https://raster.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
|
||||
<a href="https://x.com/kilocode"><img src="https://raster.shields.io/badge/kilocode-000000?style=flat&logo=x&logoColor=white" alt="X (Twitter)"></a>
|
||||
<a href="https://blog.kilo.ai"><img src="https://raster.shields.io/badge/Blog-555?style=flat&logo=substack&logoColor=white" alt="Substack Blog"></a>
|
||||
<a href="https://kilo.ai/discord"><img src="https://raster.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
|
||||
<a href="https://www.reddit.com/r/kilocode/"><img src="https://raster.shields.io/badge/Join%20r%2Fkilocode-D84315?style=flat&logo=reddit&logoColor=white" alt="Reddit"></a>
|
||||
</p>
|
||||
|
||||
# 🚀 Kilo
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# MCP Servers Sub-Tab Parity
|
||||
|
||||
**Priority:** P2
|
||||
|
||||
The legacy MCP Servers sub-tab (842 lines) had full server lifecycle management. The new sub-tab can only view and remove servers.
|
||||
|
||||
## Side-by-Side Comparison
|
||||
|
||||
| Feature | Legacy | New |
|
||||
| ----------------------- | -------------------------------- | ------------------------------------------------------------- |
|
||||
| View server list | Name + source badge + status dot | Name + command/URL |
|
||||
| Add server | Via edit config file buttons | Not available |
|
||||
| Remove server | Delete button + confirmation | Remove button + confirmation |
|
||||
| Edit server config | Via edit config file buttons | Not available |
|
||||
| Connection status | Colored dot (green/yellow/red) | Not displayed |
|
||||
| Enable/disable toggle | `ToggleSwitch` per server | Not available |
|
||||
| Restart/refresh | Per-server refresh button | Not available |
|
||||
| Refresh all | "Refresh All MCP Servers" button | Not available |
|
||||
| Edit Global MCP config | Button to open file | Not available |
|
||||
| Edit Project MCP config | Button to open file | Not available |
|
||||
| Network timeout | Per-server dropdown (15s–60min) | Not available |
|
||||
| Server source badge | Shows "global" / "project" | Not displayed |
|
||||
| Empty state | N/A | "No MCP servers configured. Edit the opencode config file..." |
|
||||
|
||||
### Expandable Server Detail (Legacy Only)
|
||||
|
||||
When a server was connected and enabled, expanding it showed 5 tabs:
|
||||
|
||||
| Tab | Content |
|
||||
| ------------ | --------------------------------------------------------------------- |
|
||||
| Tools | List of MCP tools with tool count |
|
||||
| Resources | List of resources + resource templates with count |
|
||||
| Instructions | Server-provided instruction text (if present) |
|
||||
| Logs | Error/log entries sorted by timestamp with count |
|
||||
| Auth | OAuth debug info: auth status, token expiration, scopes, refresh info |
|
||||
|
||||
The new extension has none of this expandable detail.
|
||||
|
||||
## Remaining Work
|
||||
|
||||
### Important (P2)
|
||||
|
||||
- **Add MCP Server dialog**: Form to define a new server with name, transport type (stdio/SSE), command+args or URL, environment variables. Write to CLI config via `updateConfig()`
|
||||
- **Edit MCP Server**: Allow modifying existing server configurations
|
||||
- **Connection status indicators**: Display per-server connection state. Requires CLI to expose MCP connection status via API or SSE events
|
||||
- **Enable/disable toggle**: Per-server toggle that writes to CLI config without removing the server
|
||||
- **Restart/refresh button**: Per-server and "refresh all" buttons. Requires CLI endpoint to restart MCP servers
|
||||
- **Edit config file buttons**: Quick links to open global/project MCP config in the editor
|
||||
|
||||
### Nice to Have (P3)
|
||||
|
||||
- **Expandable server detail**: Show tools, resources, instructions, and logs per server when expanded. Requires CLI to expose per-server tool/resource/log data
|
||||
- **Network timeout per server**: If CLI supports per-server timeout config, expose it
|
||||
- **Server source badge**: Display whether server comes from global or project config
|
||||
- **OAuth/Auth tab**: Display auth status for OAuth-based MCP servers (depends on CLI support)
|
||||
|
||||
## Notes
|
||||
|
||||
The mcp-and-mcp-hub.md doc in `non-agent-features/` tracks MCP configuration as a broader feature. This doc focuses specifically on the MCP Servers sub-tab within the Agent Behaviour settings tab. The Marketplace covers MCP server discovery and installation from a registry.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Modes / Agents Sub-Tab Parity
|
||||
|
||||
**Priority:** P2
|
||||
|
||||
The legacy "Modes" sub-tab was a 1794-line component with comprehensive mode management. The new "Agents" sub-tab now covers core CRUD (create, edit, delete) after PR #7225 but is still missing several legacy features.
|
||||
|
||||
## Side-by-Side Comparison
|
||||
|
||||
### Mode/Agent CRUD
|
||||
|
||||
| Feature | Legacy (Modes) | New (Agents) |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| View modes/agents list | List with name + slug | Interactive clickable list with name + description + "custom" badges |
|
||||
| Select mode to configure | Popover/Command searchable combobox | Click-to-edit in agent list |
|
||||
| Create new mode | Full dialog: name, slug, save location, role definition, description, when-to-use, tool groups, custom instructions | "Create New Mode" button with form: name (validated), description, system prompt |
|
||||
| Delete custom mode | Confirmation dialog | Delete button with confirmation dialog |
|
||||
| Rename custom mode | Inline text field with save/cancel | Not available — name is immutable after creation |
|
||||
| Export mode | Export to file button | Not available |
|
||||
| Import mode | Import dialog (project/global) | Not available (marketplace handles this) |
|
||||
| Edit modes config (JSON) | Dropdown: "Edit Global Modes" / "Edit Project Modes" | Not available |
|
||||
| Marketplace button | Opens mode marketplace | Not available |
|
||||
|
||||
### Per-Mode/Agent Settings
|
||||
|
||||
| Setting | Legacy | New |
|
||||
| ------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
||||
| API Configuration (provider profile) | `<Select>` dropdown of all API config profiles | Not applicable (CLI uses per-agent `model` instead of profiles) |
|
||||
| Role Definition / System Prompt | `<TextArea>` (5 rows) + reset button for built-ins | Multiline auto-resizing textarea (edit view for custom; prompt override for built-in) |
|
||||
| Description | `<TextField>` + reset button | Editable text field in edit view |
|
||||
| When to Use | `<TextArea>` (4 rows) + reset button | Not available |
|
||||
| Tools (tool groups) | Checkbox grid per group, file regex restrictions | Not applicable (CLI uses permission-based tool control) |
|
||||
| Custom Instructions (per-mode) | `<TextArea>` (10 rows) + link to `.kilocode/rules-{slug}/rules.md` | Not available as separate field (merged into system prompt) |
|
||||
| Temperature | Not in legacy Modes sub-tab (was in provider profile) | `<TextField>` per agent |
|
||||
| Top P | Not in legacy Modes sub-tab | `<TextField>` per agent |
|
||||
| Max Steps | Not in legacy Modes sub-tab | `<TextField>` per agent |
|
||||
| Model Override | Not in legacy Modes sub-tab (was via API config) | `<TextField>` per agent |
|
||||
|
||||
### System Prompt Features
|
||||
|
||||
| Feature | Legacy | New |
|
||||
| ---------------------- | ----------------------------------------------------------------- | ------------- |
|
||||
| Preview System Prompt | Button to view full rendered prompt in slide-out panel | Not available |
|
||||
| Copy System Prompt | Clipboard button | Not available |
|
||||
| Override System Prompt | Advanced disclosure with link to `.kilocode/system-prompt-{slug}` | Not available |
|
||||
|
||||
### Global Custom Instructions
|
||||
|
||||
| Feature | Legacy | New |
|
||||
| -------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------- |
|
||||
| Global Custom Instructions field | `<TextArea>` (4 rows) inside Modes sub-tab | Not available in Agents sub-tab (Rules sub-tab has instruction file paths instead) |
|
||||
| Link to global rules file | Link to `.kilocode/rules/rules.md` | Not available |
|
||||
|
||||
### Organization Features
|
||||
|
||||
| Feature | Legacy | New |
|
||||
| ------------------------- | ------------------------------------------------- | ------------- |
|
||||
| Share modes banner | `KiloShareModesBanner` component | Not available |
|
||||
| Organization mode warning | Warning when mode has `source === "organization"` | Not available |
|
||||
|
||||
### CLI Agent Fields Not Exposed
|
||||
|
||||
These are CLI capabilities beyond what the legacy had:
|
||||
|
||||
| CLI Field | Type | Description |
|
||||
| ------------------------ | ------------------------------ | ---------------------------------------------------- |
|
||||
| `agent[name].variant` | string | Default thinking/reasoning variant (low/medium/high) |
|
||||
| `agent[name].permission` | Permission | Per-agent tool permission overrides |
|
||||
| `agent[name].hidden` | boolean | Hide agent from mode switcher |
|
||||
| `agent[name].disable` | boolean | Fully disable agent |
|
||||
| `agent[name].mode` | "subagent" / "primary" / "all" | Agent visibility context |
|
||||
| `agent[name].color` | string | Agent identification color |
|
||||
| `agent[name].options` | Record | Arbitrary key-value config |
|
||||
|
||||
## Remaining Work
|
||||
|
||||
### Important (P2)
|
||||
|
||||
- **When to Use field**: Add a text area for orchestrator routing hints (if CLI supports it)
|
||||
- **System prompt preview**: Add a button to view the full rendered system prompt — requires a CLI endpoint to return the assembled prompt for an agent
|
||||
- **Import/Export**: Add import (from file) and export (to file) for agent/mode definitions
|
||||
- **Default variant per agent**: Expose `agent[name].variant` for persistent thinking effort defaults
|
||||
- **Hidden/disable toggles**: Add visibility controls for agents
|
||||
|
||||
### Nice to Have (P3)
|
||||
|
||||
- **Rename agent**: Allow renaming custom agents (currently name is immutable after creation)
|
||||
- **Edit config JSON buttons**: Quick links to open the raw config file for global/project agent config
|
||||
- **Marketplace integration**: Button to open mode marketplace (covered by marketplace.md)
|
||||
- **Organization mode features**: Share modes banner and org mode warning (depends on org feature implementation)
|
||||
- **Agent mode selector**: Dropdown for subagent/primary/all
|
||||
- **Color picker**: Visual agent identification
|
||||
- **Per-agent permissions**: Collapsible permission override section per agent
|
||||
- **Copy system prompt**: Clipboard button for the assembled prompt
|
||||
@@ -0,0 +1,53 @@
|
||||
# Rules & Workflows Sub-Tabs Parity
|
||||
|
||||
**Priority:** P2 (Rules), P3 (Workflows)
|
||||
|
||||
The legacy extension had feature-complete Rules and Workflows sub-tabs with toggle lists, file creation, and global/workspace separation. The new extension has a simpler Rules sub-tab and a stub Workflows sub-tab.
|
||||
|
||||
## Rules Sub-Tab Comparison
|
||||
|
||||
| Feature | Legacy | New |
|
||||
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| Description text + docs link | Yes | No |
|
||||
| Global rules section | Toggle list of global rule files | Not separated — single flat list of instruction file paths |
|
||||
| Workspace rules section | Toggle list of workspace rule files | Same flat list |
|
||||
| Per-rule enable/disable toggle | `ToggleSwitch` per rule file | Not available — paths are either in the list or removed |
|
||||
| Create new rule file | Input field with validation (.md, .txt, or no extension) per section | Not available |
|
||||
| Add instruction file path | Not available (auto-discovered from filesystem) | `TextField` + "Add" button |
|
||||
| Rule source discovery | Auto-discovers from `.kilocode/rules/`, `.kilocoderules`, `.roorules`, `.clinerules`, mode-specific dirs, `AGENTS.md` | Only shows explicitly configured paths in `config.instructions` |
|
||||
|
||||
### Key Architectural Difference
|
||||
|
||||
The legacy extension auto-discovered rule files from well-known filesystem locations and displayed them with on/off toggles. The new extension treats instruction files as explicit config entries — you add paths to `config.instructions` and they're always active. There's no discovery or toggling.
|
||||
|
||||
## Workflows Sub-Tab Comparison
|
||||
|
||||
| Feature | Legacy | New |
|
||||
| ---------------------------------- | --------------------------------------- | ----------------------------------------- |
|
||||
| Description text + docs link | Yes | Placeholder: "Not yet implemented." |
|
||||
| Global workflows section | Toggle list of global workflow files | Not available |
|
||||
| Workspace workflows section | Toggle list of workspace workflow files | Not available |
|
||||
| Per-workflow enable/disable toggle | `ToggleSwitch` per workflow file | Not available |
|
||||
| Create new workflow file | Input field per section | Not available |
|
||||
| Invoke workflow | Type `/workflow-name` in chat | CLI has custom commands (similar concept) |
|
||||
|
||||
## Remaining Work
|
||||
|
||||
### Rules (P2)
|
||||
|
||||
- **Description text**: Add explanatory description with link to docs, matching the legacy pattern
|
||||
- **Global vs workspace separation**: If the CLI distinguishes between global and workspace instruction files, separate them into two sections
|
||||
- **Enable/disable toggles**: If rules can be toggled without removing them from config, add toggle switches per rule
|
||||
- **New rule file creation**: Add a button/input to create a new rule file (not just add an existing path)
|
||||
- **Auto-discovery**: Consider adding a section showing auto-discovered rule sources (`.kilocode/rules/`, `AGENTS.md`) that the CLI loads regardless of explicit `config.instructions` entries
|
||||
|
||||
### Workflows (P3)
|
||||
|
||||
- **Implement the sub-tab**: Replace the placeholder with actual workflow management
|
||||
- **Determine CLI mapping**: The CLI has custom commands — determine whether "workflows" maps to CLI custom commands or is a distinct concept
|
||||
- **Workflow file management**: Add toggle list for global/workspace workflow files with create/enable/disable
|
||||
- **Description text**: Add explanatory description matching the legacy pattern
|
||||
|
||||
## Notes
|
||||
|
||||
The existing `non-agent-features/rules-and-workflows.md` doc covers workflows broadly. This doc adds the specific UI comparison for what the sub-tabs look like in the Agent Behaviour tab. The CLI's `config.instructions` array is a simpler model than the legacy's auto-discovered toggle list — the extension may need to augment it with discovery logic to match parity.
|
||||
@@ -12,3 +12,19 @@
|
||||
- Write approved settings to CLI config via `/global/config` endpoint or directly to `opencode.json`
|
||||
- Show what was migrated and what was not
|
||||
- Mark migration as complete in `globalState` so it doesn't run again
|
||||
|
||||
## Agent Behaviour Tab Settings to Migrate
|
||||
|
||||
Settings from the legacy "Agent Behaviour" tab (Modes, MCP Servers, Rules, Workflows, Skills sub-tabs):
|
||||
|
||||
| Legacy Setting | CLI Equivalent | Notes |
|
||||
| ----------------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| `customModes` (custom mode definitions) | CLI agent config in `opencode.json` | Map `roleDefinition` → `prompt`, `groups` → CLI tool permissions |
|
||||
| `customModePrompts` (per-mode overrides) | Per-agent `prompt`, `temperature`, etc. | Map each mode slug to CLI agent name |
|
||||
| `customInstructions` (global, shown in Modes sub-tab) | Rule files in `.kilocode/rules/` or `config.instructions` | May need a global instructions config key in CLI |
|
||||
| `modeApiConfigs` (per-mode model) | Per-agent `model` | Map mode slug → agent name → model ID |
|
||||
| MCP server configs | `config.mcp` | CLI owns MCP config |
|
||||
| `localRulesToggles` / `globalRulesToggles` | `config.instructions` | Toggle state doesn't map directly — CLI has path list |
|
||||
| `localWorkflowToggles` / `globalWorkflowToggles` | CLI custom commands (TBD) | Workflow concept mapping needs clarification |
|
||||
|
||||
See [Agent Behaviour Tab Parity](../agent-behaviour/) docs for detailed sub-tab comparisons.
|
||||
|
||||
@@ -10,3 +10,8 @@
|
||||
- **Prompts tab**: Shows "Not implemented" message
|
||||
- **Workflows subtab** (inside AgentBehaviour): Renders a `<Placeholder>` with no functionality
|
||||
- Import/export settings functionality
|
||||
|
||||
## Related Docs
|
||||
|
||||
- **Agent Behaviour tab** (all sub-tabs) → [Agent Behaviour Tab Parity](../agent-behaviour/)
|
||||
- **Workflows subtab** → [Rules & Workflows Sub-Tabs Parity](../agent-behaviour/rules-workflows-subtab-parity.md)
|
||||
|
||||
@@ -42,6 +42,19 @@ This document tracks remaining work needed for feature parity with the old exten
|
||||
|
||||
---
|
||||
|
||||
## Agent Behaviour Tab Parity
|
||||
|
||||
The "Agent Behaviour" settings tab contains 5 sub-tabs in both the legacy and new extensions. The legacy tab was a combined 2800+ lines of UI; the new tab is ~820 lines. Each sub-tab has its own parity doc.
|
||||
|
||||
| Sub-Tab | Remaining Work | Priority |
|
||||
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
||||
| [Modes / Agents](agent-behaviour/modes-subtab-parity.md) | Core CRUD done (PR #7225). Remaining: when-to-use, system prompt preview, import/export, default variant, hidden/disable, org features | P2 |
|
||||
| [MCP Servers](agent-behaviour/mcp-server-creation.md) | Add/edit servers, connection status, enable/disable toggle, restart, per-server timeout, expandable detail (tools/resources/logs/auth) | P2 |
|
||||
| [Rules & Workflows](agent-behaviour/rules-workflows-subtab-parity.md) | Rules: description text, global/workspace separation, per-rule toggles, new file creation, auto-discovery. Workflows: entire sub-tab is a stub | P2/P3 |
|
||||
| Skills | Minor gaps: project/global separation, mode badge per skill. Covered by [Skills System](non-agent-features/skills-system.md) | P2 |
|
||||
|
||||
---
|
||||
|
||||
## Project Board Issues
|
||||
|
||||
Open issues from the [GitHub project board](https://github.com/orgs/Kilo-Org/projects/25/views/1) not covered by the feature docs above. Each item has its own detailed doc.
|
||||
|
||||
@@ -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.1.2",
|
||||
"version": "7.1.8",
|
||||
"icon": "assets/icons/logo-outline-black.png",
|
||||
"galleryBanner": {
|
||||
"color": "#FFFFFF",
|
||||
@@ -17,6 +17,7 @@
|
||||
"engines": {
|
||||
"vscode": "^1.105.1"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Kilo Code"
|
||||
},
|
||||
@@ -89,11 +90,6 @@
|
||||
"title": "History",
|
||||
"icon": "$(history)"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.cloudHistoryButtonClicked",
|
||||
"title": "Cloud History",
|
||||
"icon": "$(cloud)"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.profileButtonClicked",
|
||||
"title": "Profile",
|
||||
@@ -333,33 +329,28 @@
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManagerOpen",
|
||||
"command": "kilo-code.new.historyButtonClicked",
|
||||
"group": "navigation@1",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.marketplaceButtonClicked",
|
||||
"command": "kilo-code.new.agentManagerOpen",
|
||||
"group": "navigation@2",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.cloudHistoryButtonClicked",
|
||||
"command": "kilo-code.new.marketplaceButtonClicked",
|
||||
"group": "navigation@3",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.historyButtonClicked",
|
||||
"command": "kilo-code.new.profileButtonClicked",
|
||||
"group": "navigation@4",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.profileButtonClicked",
|
||||
"group": "navigation@5",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.settingsButtonClicked",
|
||||
"group": "navigation@6",
|
||||
"group": "navigation@5",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
}
|
||||
],
|
||||
@@ -753,7 +744,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"prepare:cli-binary": "bun script/local-bin.ts",
|
||||
"compile": "bun run prepare:cli-binary && bun run rebuild-sdk && bun run typecheck && bun run lint && node esbuild.js",
|
||||
"compile": "bun run prepare:cli-binary -- --force && bun run rebuild-sdk && bun run typecheck && bun run lint && node esbuild.js",
|
||||
"watch": "bun run rebuild-sdk && bun run --parallel watch:esbuild watch:tsc",
|
||||
"watch:esbuild": "bun run prepare:cli-binary && node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
|
||||
@@ -36,9 +36,10 @@ const targets = [
|
||||
const binDir = join(import.meta.dir, "..", "bin")
|
||||
const distDir = join(import.meta.dir, "..", "dist")
|
||||
const outDir = join(import.meta.dir, "..", "out")
|
||||
const outDirProd = join(import.meta.dir, "..", "out", "prod")
|
||||
|
||||
console.log("\n🧹 Cleaning up directories...")
|
||||
for (const dir of [binDir, distDir, outDir]) {
|
||||
for (const dir of [binDir, distDir, outDir, outDirProd]) {
|
||||
if (existsSync(dir)) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
console.log(` ✓ Cleaned ${dir}`)
|
||||
@@ -47,6 +48,7 @@ for (const dir of [binDir, distDir, outDir]) {
|
||||
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
mkdirSync(distDir, { recursive: true })
|
||||
mkdirSync(outDirProd, { recursive: true })
|
||||
|
||||
console.log("\n🔄 Rebuilding SDK types (ensures dist/ is in sync with server API)...")
|
||||
await $`bun run --cwd ${join(import.meta.dir, "..", "..", "sdk", "js")} build`
|
||||
@@ -87,6 +89,13 @@ for (const config of targets) {
|
||||
npm_config_ignore_scripts: "true",
|
||||
})
|
||||
console.log(` ✅ Created ${vsixPath}`)
|
||||
|
||||
const prodVsixPath = join(outDirProd, `kilo-vscode-${config.target}.vsix`)
|
||||
await $`vsce package --no-dependencies --skip-license --target ${config.target} -o ${prodVsixPath}`.env({
|
||||
...process.env,
|
||||
npm_config_ignore_scripts: "true",
|
||||
})
|
||||
console.log(` ✅ Created ${prodVsixPath}`)
|
||||
}
|
||||
|
||||
console.log("\n✨ All VSIX packages built successfully!")
|
||||
|
||||
@@ -24,11 +24,42 @@ const opencodeDir = join(packagesDir, "opencode")
|
||||
const targetBinDir = join(kiloVscodeDir, "bin")
|
||||
const binName = process.platform === "win32" ? "kilo.exe" : "kilo"
|
||||
const targetBinPath = join(targetBinDir, binName)
|
||||
const versionFile = join(targetBinDir, ".cli-version")
|
||||
|
||||
function log(msg: string) {
|
||||
console.log(`[local-bin] ${msg}`)
|
||||
}
|
||||
|
||||
async function cliSourceHash(): Promise<string | null> {
|
||||
try {
|
||||
const result = await $`git log -1 --format=%H -- .`.cwd(opencodeDir).quiet()
|
||||
return result.text().trim() || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function isDirty(): Promise<boolean> {
|
||||
try {
|
||||
const result = await $`git status --porcelain -- .`.cwd(opencodeDir).quiet()
|
||||
return result.text().trim().length > 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function isStale(): Promise<boolean> {
|
||||
if (await isDirty()) return true
|
||||
const hash = await cliSourceHash()
|
||||
if (!hash) return false // can't determine — assume fresh
|
||||
try {
|
||||
const stored = (await Bun.file(versionFile).text()).trim()
|
||||
return stored !== hash
|
||||
} catch {
|
||||
return true // no version file — treat as stale
|
||||
}
|
||||
}
|
||||
|
||||
function platformTag(): string {
|
||||
const os = process.platform === "win32" ? "windows" : process.platform
|
||||
return `cli-${os}-${process.arch}`
|
||||
@@ -114,8 +145,12 @@ async function ensureBuiltBinary(): Promise<string> {
|
||||
|
||||
async function main() {
|
||||
const targetFile = Bun.file(targetBinPath)
|
||||
const exists = await targetFile.exists()
|
||||
|
||||
if ((await targetFile.exists()) && !forceRebuild) {
|
||||
const stale = exists && !forceRebuild && (await isStale())
|
||||
const rebuild = forceRebuild || stale
|
||||
|
||||
if (exists && !rebuild) {
|
||||
const st = statSync(targetBinPath)
|
||||
log(
|
||||
`CLI binary already present at ${relative(kiloVscodeDir, targetBinPath)} (${Math.round(st.size / 1024 / 1024)}MB). Use --force to rebuild.`,
|
||||
@@ -123,8 +158,8 @@ async function main() {
|
||||
return
|
||||
}
|
||||
|
||||
if ((await targetFile.exists()) && forceRebuild) {
|
||||
log(`Removing existing binary (--force).`)
|
||||
if (exists && rebuild) {
|
||||
log(stale ? `CLI source has changed — rebuilding.` : `Removing existing binary (--force).`)
|
||||
rmSync(targetBinPath)
|
||||
// Also remove the prebuilt dist so ensureBuiltBinary() triggers a fresh build
|
||||
const distDir = join(opencodeDir, "dist")
|
||||
@@ -144,6 +179,10 @@ async function main() {
|
||||
await $`cp ${sourceBinPath} ${targetBinPath}`
|
||||
chmodSync(targetBinPath, 0o755)
|
||||
|
||||
// Record the CLI source version so future runs detect when a rebuild is needed
|
||||
const hash = await cliSourceHash()
|
||||
if (hash) await Bun.write(versionFile, hash + "\n")
|
||||
|
||||
log(`Copied CLI binary from ${relative(packagesDir, sourceBinPath)} -> ${relative(kiloVscodeDir, targetBinPath)}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Script } from "@opencode-ai/script"
|
||||
console.log(`Publishing VSCode extension for release: v${Script.version}`)
|
||||
|
||||
const outDir = process.env.VSIX_DIR || join(import.meta.dir, "..", "out")
|
||||
const outDirProd = join(outDir, "prod")
|
||||
|
||||
console.log(`Using VSIX directory: ${outDir}`)
|
||||
|
||||
if (!existsSync(outDir)) {
|
||||
@@ -37,15 +39,14 @@ console.log(`\nFound ${vsixFiles.length} VSIX files`)
|
||||
|
||||
for (const target of targets) {
|
||||
const vsixPath = join(outDir, `kilo-vscode-${target}.vsix`)
|
||||
|
||||
console.log(`\n🚀 Publishing ${target} to VS Code Marketplace...`)
|
||||
await $`vsce publish --pre-release --packagePath ${vsixPath}`
|
||||
console.log(` ✅ Published ${target} to VS Code Marketplace`)
|
||||
|
||||
// Note: Open VSX publishing is commented out as it doesn't support prereleases
|
||||
// console.log(`\n📤 Publishing ${target} to Open VSX...`)
|
||||
// await $`npx ovsx publish ${vsixPath} --target ${target} -p ${process.env.OPENVSX_TOKEN}`
|
||||
// console.log(` ✅ Published ${target} to Open VSX`)
|
||||
const prodVsixPath = join(outDirProd, `kilo-vscode-${target}.vsix`)
|
||||
console.log(`\n📤 Publishing ${target} to Open VSX...`)
|
||||
await $`npx ovsx publish --pat ${process.env.OPENVSX_TOKEN} --packagePath ${prodVsixPath}`
|
||||
console.log(` ✅ Published Prod ${target} to Open VSX`)
|
||||
}
|
||||
|
||||
if (Script.release) {
|
||||
|
||||
@@ -28,8 +28,13 @@ import {
|
||||
isEventFromForeignProject,
|
||||
loadSessions as loadSessionsUtil,
|
||||
flushPendingSessionRefresh as flushPendingSessionRefreshUtil,
|
||||
resolveContextDirectory,
|
||||
resolveWorkspaceDirectory,
|
||||
type SessionRefreshContext,
|
||||
} from "./kilo-provider-utils"
|
||||
import { GitOps } from "./agent-manager/GitOps"
|
||||
import { GitStatsPoller, type LocalStats } from "./agent-manager/GitStatsPoller"
|
||||
import { getWorkspaceRoot } from "./review-utils"
|
||||
import { MarketplaceService } from "./services/marketplace"
|
||||
import { resolveProjectDirectory } from "./project-directory"
|
||||
import { getBusySessionCount, seedSessionStatuses } from "./session-status"
|
||||
@@ -75,6 +80,7 @@ import {
|
||||
disconnectProvider as disconnectProviderAction,
|
||||
saveCustomProvider as saveCustomProviderAction,
|
||||
} from "./provider-actions"
|
||||
import { fetchOpenAIModels, FetchModelsError } from "./shared/fetch-models"
|
||||
|
||||
type KiloProviderOptions = {
|
||||
projectDirectory?: string | null
|
||||
@@ -86,6 +92,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
|
||||
private webview: vscode.Webview | null = null
|
||||
private currentSession: Session | null = null
|
||||
/** Remembers the last selected session so /new can stay in the same worktree after clearSession. */
|
||||
private contextSessionID: string | undefined
|
||||
private connectionState: "connecting" | "connected" | "disconnected" | "error" = "connecting"
|
||||
private loginAttempt = 0
|
||||
private isWebviewReady = false
|
||||
@@ -143,11 +151,19 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
private chatAutocomplete: ChatTextAreaAutocomplete | null = null
|
||||
private projectDirectory: string | null | undefined
|
||||
private slimEditMetadata = true
|
||||
/** Worktree diff stats poller for the sidebar badge — reuses GitStatsPoller (local stats only) */
|
||||
private statsPoller: GitStatsPoller | null = null
|
||||
private cachedStats: unknown = null
|
||||
|
||||
/** Optional interceptor called before the standard message handler.
|
||||
* Return null to consume the message, or return a (possibly transformed) message. */
|
||||
private onBeforeMessage: ((msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>) | null = null
|
||||
|
||||
/** Handler for "Continue in Worktree" — set by extension.ts to delegate to AgentManagerProvider. */
|
||||
private continueInWorktreeHandler:
|
||||
| ((sessionId: string, progress: (status: string, detail?: string, error?: string) => void) => Promise<void>)
|
||||
| null = null
|
||||
|
||||
constructor(
|
||||
private readonly extensionUri: vscode.Uri,
|
||||
private readonly connectionService: KiloConnectionService,
|
||||
@@ -257,6 +273,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
data: profileData,
|
||||
})
|
||||
|
||||
// Re-send cached worktree stats so the badge renders immediately after webview reload.
|
||||
if (this.cachedStats) this.postMessage(this.cachedStats)
|
||||
|
||||
// Seed session status map so the Settings panel knows about already-running sessions.
|
||||
// Must run after webview is ready (postMessage is a no-op before that).
|
||||
void this.seedSessionStatusMap()
|
||||
@@ -294,6 +313,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
// Handle messages from webview (shared handler)
|
||||
this.setupWebviewMessageHandler(webviewView.webview)
|
||||
|
||||
// Pause stats polling when sidebar is hidden, resume when visible
|
||||
webviewView.onDidChangeVisibility(() => {
|
||||
this.statsPoller?.setEnabled(webviewView.visible)
|
||||
})
|
||||
|
||||
// Initialize connection to CLI backend
|
||||
this.initializeConnection()
|
||||
}
|
||||
@@ -325,6 +349,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
*/
|
||||
public registerSession(session: Session): void {
|
||||
this.currentSession = session
|
||||
this.contextSessionID = session.id
|
||||
this.trackedSessionIds.add(session.id)
|
||||
this.postMessage({
|
||||
type: "sessionCreated",
|
||||
@@ -374,6 +399,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
this.postMessage({ type: "openCloudSession", sessionId })
|
||||
}
|
||||
|
||||
/** Register the handler for "Continue in Worktree" messages from the sidebar. */
|
||||
public setContinueInWorktreeHandler(
|
||||
handler: (sessionId: string, progress: (status: string, detail?: string, error?: string) => void) => Promise<void>,
|
||||
): void {
|
||||
this.continueInWorktreeHandler = handler
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach to a webview that already has its own HTML set.
|
||||
* Sets up message handling and connection without overriding HTML content.
|
||||
@@ -494,6 +526,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
await this.handleCreateSession()
|
||||
break
|
||||
case "clearSession":
|
||||
this.contextSessionID = this.currentSession?.id ?? this.contextSessionID
|
||||
this.currentSession = null
|
||||
break
|
||||
case "loadMessages":
|
||||
@@ -540,6 +573,20 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
case "openChanges":
|
||||
vscode.commands.executeCommand("kilo-code.new.showChanges")
|
||||
break
|
||||
case "continueInWorktree":
|
||||
if (message.sessionId && this.continueInWorktreeHandler) {
|
||||
this.continueInWorktreeHandler(message.sessionId, (status: string, detail?: string, error?: string) => {
|
||||
this.postMessage({ type: "continueInWorktreeProgress", status, detail, error })
|
||||
}).catch((err: unknown) => {
|
||||
console.error("[Kilo New] continueInWorktree failed:", err)
|
||||
this.postMessage({
|
||||
type: "continueInWorktreeProgress",
|
||||
status: "error",
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
break
|
||||
case "retryConnection":
|
||||
console.log("[Kilo New] KiloProvider: 🔄 Retrying connection...")
|
||||
this.initializeConnection().catch((e) =>
|
||||
@@ -567,6 +614,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
case "saveCustomProvider":
|
||||
await this.handleProviderAction(message)
|
||||
break
|
||||
case "fetchCustomProviderModels":
|
||||
this.handleFetchCustomProviderModels(message).catch((e) =>
|
||||
console.error("[Kilo New] fetchCustomProviderModels failed:", e),
|
||||
)
|
||||
break
|
||||
case "compact":
|
||||
await this.handleCompact(message.sessionID, message.providerID, message.modelID)
|
||||
break
|
||||
@@ -590,6 +642,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
case "removeMcp":
|
||||
this.handleRemoveMcp(message.name).catch((e) => console.error("[Kilo New] handleRemoveMcp failed:", e))
|
||||
break
|
||||
|
||||
case "questionReply":
|
||||
await handleQuestionReply(this.questionCtx, message.requestID, message.answers)
|
||||
break
|
||||
@@ -599,6 +652,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
case "requestConfig":
|
||||
this.fetchAndSendConfig().catch((e) => console.error("[Kilo New] fetchAndSendConfig failed:", e))
|
||||
break
|
||||
case "requestGlobalConfig":
|
||||
this.fetchAndSendGlobalConfig().catch((e) => console.error("[Kilo New] fetchAndSendGlobalConfig failed:", e))
|
||||
break
|
||||
case "updateConfig":
|
||||
await this.handleUpdateConfig(message.config)
|
||||
break
|
||||
@@ -971,6 +1027,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
])
|
||||
this.sendNotificationSettings()
|
||||
|
||||
// Start polling worktree diff stats for the sidebar badge
|
||||
this.startStatsPolling()
|
||||
|
||||
console.log("[Kilo New] KiloProvider: ✅ initializeConnection completed successfully")
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: ❌ Failed to initialize connection:", error)
|
||||
@@ -1004,9 +1063,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
}
|
||||
|
||||
try {
|
||||
const workspaceDir = this.getWorkspaceDirectory()
|
||||
const workspaceDir = this.getContextDirectory()
|
||||
const { data: session } = await this.client.session.create({ directory: workspaceDir }, { throwOnError: true })
|
||||
this.currentSession = session
|
||||
this.contextSessionID = session.id
|
||||
this.trackDirectory(session.id, workspaceDir)
|
||||
this.trackedSessionIds.add(session.id)
|
||||
|
||||
// Notify webview of the new session
|
||||
@@ -1029,6 +1090,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
private async handleLoadMessages(sessionID: string): Promise<void> {
|
||||
// Track the session so we receive its SSE events
|
||||
this.trackedSessionIds.add(sessionID)
|
||||
this.contextSessionID = sessionID
|
||||
|
||||
if (!this.client) {
|
||||
this.postMessage({
|
||||
@@ -1067,6 +1129,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
.then((result) => {
|
||||
if (result.data && !abort.signal.aborted) {
|
||||
this.currentSession = result.data
|
||||
this.contextSessionID = result.data.id
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => console.warn("[Kilo New] KiloProvider: getSession failed (non-critical):", err))
|
||||
@@ -1385,6 +1448,22 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
return saveCustomProviderAction(ctx, rid, pid, config, key, this.cachedConfigMessage, set)
|
||||
}
|
||||
|
||||
private async handleFetchCustomProviderModels(msg: Record<string, unknown>): Promise<void> {
|
||||
const rid = typeof msg.requestId === "string" ? msg.requestId : ""
|
||||
const url = typeof msg.baseURL === "string" ? msg.baseURL : ""
|
||||
if (!rid || !url) return
|
||||
const key = typeof msg.apiKey === "string" ? msg.apiKey : undefined
|
||||
const headers = msg.headers && typeof msg.headers === "object" ? (msg.headers as Record<string, string>) : undefined
|
||||
try {
|
||||
const models = await fetchOpenAIModels({ baseURL: url, apiKey: key, headers })
|
||||
this.postMessage({ type: "customProviderModelsFetched", requestId: rid, models })
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : "Failed to fetch models"
|
||||
const auth = err instanceof FetchModelsError && err.auth
|
||||
this.postMessage({ type: "customProviderModelsFetched", requestId: rid, error: message, auth })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch agents (modes) from the backend and send to webview.
|
||||
*/
|
||||
@@ -1406,6 +1485,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
type: "agentsLoaded",
|
||||
agents: visible.map((a) => ({
|
||||
name: a.name,
|
||||
displayName: a.displayName,
|
||||
description: a.description,
|
||||
mode: a.mode,
|
||||
native: a.native,
|
||||
@@ -1662,6 +1742,17 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch global-only config (no project/managed layers) for settings export. */
|
||||
private async fetchAndSendGlobalConfig(): Promise<void> {
|
||||
if (!this.client || this.connectionState !== "connected") return
|
||||
try {
|
||||
const { data: config } = await this.client.global.config.get({ throwOnError: true })
|
||||
this.postMessage({ type: "globalConfigLoaded", config })
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to fetch global config:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed sessionStatusMap with current session statuses on connect.
|
||||
* Without this, the Settings panel (which has no tracked sessions) would see
|
||||
@@ -1853,11 +1944,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
private async resolveSession(sessionID?: string): Promise<{ sid: string; dir: string } | undefined> {
|
||||
if (!this.client) return undefined
|
||||
|
||||
const dir = this.getWorkspaceDirectory(sessionID || this.currentSession?.id)
|
||||
const dir = sessionID ? this.getWorkspaceDirectory(sessionID) : this.getContextDirectory()
|
||||
|
||||
if (!sessionID && !this.currentSession) {
|
||||
const { data: session } = await this.client.session.create({ directory: dir }, { throwOnError: true })
|
||||
this.currentSession = session
|
||||
this.contextSessionID = session.id
|
||||
this.trackDirectory(session.id, dir)
|
||||
this.trackedSessionIds.add(session.id)
|
||||
this.postMessage({
|
||||
type: "sessionCreated",
|
||||
@@ -2117,6 +2210,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
},
|
||||
set currentSession(session) {
|
||||
self.currentSession = session
|
||||
if (session) self.contextSessionID = session.id
|
||||
},
|
||||
trackedSessionIds: this.trackedSessionIds,
|
||||
connectionService: this.connectionService,
|
||||
@@ -2135,6 +2229,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
getWorkspaceDirectory: () => this.getWorkspaceDirectory(),
|
||||
disposeGlobal: () => this.disposeGlobal(),
|
||||
fetchAndSendProviders: () => this.fetchAndSendProviders(),
|
||||
fetchAndSendAgents: () => this.fetchAndSendAgents(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2256,7 +2351,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
if (!key.startsWith(prefix)) continue
|
||||
const parts = key.split(".")
|
||||
const section = parts.slice(0, -1).join(".")
|
||||
const leaf = parts[parts.length - 1]
|
||||
const leaf = parts[parts.length - 1]!
|
||||
const config = vscode.workspace.getConfiguration(section)
|
||||
await config.update(leaf, undefined, vscode.ConfigurationTarget.Global)
|
||||
}
|
||||
@@ -2355,10 +2450,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
// Side effects that must happen before the webview message is sent
|
||||
if (event.type === "session.created" && !this.currentSession) {
|
||||
this.currentSession = event.properties.info
|
||||
this.contextSessionID = event.properties.info.id
|
||||
this.trackedSessionIds.add(event.properties.info.id)
|
||||
}
|
||||
if (event.type === "session.updated" && this.currentSession?.id === event.properties.info.id) {
|
||||
this.currentSession = event.properties.info
|
||||
this.contextSessionID = event.properties.info.id
|
||||
}
|
||||
|
||||
const msg = mapSSEEventToWebviewMessage(event, sessionID)
|
||||
@@ -2469,7 +2566,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
const uri = tab.input.uri
|
||||
if (uri.scheme === "file") {
|
||||
const rel = path.relative(dir, uri.fsPath)
|
||||
if (!rel.startsWith("..") && controller.validateAccess(uri.fsPath)) {
|
||||
if (!rel.startsWith("..") && !path.isAbsolute(rel) && controller.validateAccess(uri.fsPath)) {
|
||||
result.add(rel.replaceAll("\\", "/"))
|
||||
}
|
||||
}
|
||||
@@ -2503,7 +2600,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
return undefined
|
||||
}
|
||||
const relative = path.relative(workspaceDir, fsPath)
|
||||
if (relative.startsWith("..")) {
|
||||
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
return undefined
|
||||
}
|
||||
return relative
|
||||
@@ -2542,17 +2639,38 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
* Checks session directory overrides first (e.g., worktree paths), then falls back to workspace root.
|
||||
*/
|
||||
private getWorkspaceDirectory(sessionId?: string): string {
|
||||
if (sessionId) {
|
||||
const dir = this.sessionDirectories.get(sessionId)
|
||||
if (dir) return dir
|
||||
}
|
||||
return resolveWorkspaceDirectory({
|
||||
sessionID: sessionId,
|
||||
sessionDirectories: this.sessionDirectories,
|
||||
workspaceDirectory: this.getRootDirectory(),
|
||||
})
|
||||
}
|
||||
|
||||
private getContextDirectory(): string {
|
||||
return resolveContextDirectory({
|
||||
currentSessionID: this.currentSession?.id,
|
||||
contextSessionID: this.contextSessionID,
|
||||
sessionDirectories: this.sessionDirectories,
|
||||
workspaceDirectory: this.getRootDirectory(),
|
||||
})
|
||||
}
|
||||
|
||||
private getRootDirectory(): string {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
return workspaceFolders[0].uri.fsPath
|
||||
return workspaceFolders[0]!.uri.fsPath
|
||||
}
|
||||
return process.cwd()
|
||||
}
|
||||
|
||||
private trackDirectory(sessionId: string, dir: string) {
|
||||
if (path.resolve(dir) === path.resolve(this.getRootDirectory())) {
|
||||
this.sessionDirectories.delete(sessionId)
|
||||
return
|
||||
}
|
||||
this.sessionDirectories.set(sessionId, dir)
|
||||
}
|
||||
|
||||
private getProjectDirectory(sessionId?: string): string | undefined {
|
||||
return resolveProjectDirectory(this.projectDirectory, () => this.getWorkspaceDirectory(sessionId))
|
||||
}
|
||||
@@ -2601,11 +2719,38 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
return this.marketplace
|
||||
}
|
||||
|
||||
// ── Worktree stats polling (sidebar diff badge) ──────────────────
|
||||
|
||||
private startStatsPolling(): void {
|
||||
this.statsPoller?.stop()
|
||||
const git = new GitOps({ log: () => {} })
|
||||
this.statsPoller = new GitStatsPoller({
|
||||
getWorktrees: () => [],
|
||||
getWorkspaceRoot: () => getWorkspaceRoot(),
|
||||
getClient: () => this.connectionService.getClient(),
|
||||
git,
|
||||
onStats: () => {},
|
||||
onLocalStats: (stats: LocalStats) => {
|
||||
const msg = {
|
||||
type: "worktreeStatsLoaded" as const,
|
||||
files: stats.files,
|
||||
additions: stats.additions,
|
||||
deletions: stats.deletions,
|
||||
}
|
||||
this.cachedStats = msg
|
||||
this.postMessage(msg)
|
||||
},
|
||||
log: () => {},
|
||||
})
|
||||
this.statsPoller.setEnabled(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the provider and clean up subscriptions.
|
||||
* Does NOT kill the server — that's the connection service's job.
|
||||
*/
|
||||
dispose(): void {
|
||||
this.statsPoller?.stop()
|
||||
this.unsubscribeEvent?.()
|
||||
this.unsubscribeState?.()
|
||||
this.unsubscribeNotificationDismiss?.()
|
||||
|
||||
@@ -13,10 +13,12 @@ import { versionedName } from "./branch-name"
|
||||
import { normalizePath, classifyWorktreeError } from "./git-import"
|
||||
import { SetupScriptService } from "./SetupScriptService"
|
||||
import { SetupScriptRunner } from "./SetupScriptRunner"
|
||||
import { copyEnvFiles } from "./env-copy"
|
||||
import { SessionTerminalManager } from "./SessionTerminalManager"
|
||||
import { createTerminalHost } from "./terminal-host"
|
||||
import { executeVscodeTask } from "./task-runner"
|
||||
import { forkSession } from "./fork-session"
|
||||
import { continueInWorktree } from "./continue-in-worktree"
|
||||
import { shouldStopDiffPolling } from "./delete-worktree"
|
||||
import { buildKeybindingMap } from "./format-keybinding"
|
||||
import { resolveVersionModels, buildInitialMessages, type CreatedVersion } from "./multi-version"
|
||||
@@ -102,6 +104,7 @@ export class AgentManagerProvider implements Disposable {
|
||||
if (this.panel) {
|
||||
this.log("Panel already open, revealing")
|
||||
this.panel.reveal()
|
||||
this.postToWebview({ type: "action", action: "focusInput" })
|
||||
return
|
||||
}
|
||||
this.log("Opening Agent Manager panel")
|
||||
@@ -117,6 +120,11 @@ export class AgentManagerProvider implements Disposable {
|
||||
/** Restore the Agent Manager panel from a previously serialized state.
|
||||
* The caller (extension.ts / vscode-host.ts) wraps the raw panel before passing it. */
|
||||
public deserializePanel(ctx: PanelContext): void {
|
||||
if (this.panel) {
|
||||
this.log("Panel already exists during deserialization, disposing duplicate")
|
||||
ctx.dispose()
|
||||
return
|
||||
}
|
||||
this.log("Deserializing Agent Manager panel")
|
||||
this.attachPanel(ctx)
|
||||
}
|
||||
@@ -128,6 +136,11 @@ export class AgentManagerProvider implements Disposable {
|
||||
|
||||
/** Wire up a panel context (shared by openPanel and deserializePanel). */
|
||||
private attachPanel(ctx: PanelContext): void {
|
||||
if (this.panel) {
|
||||
this.log("Disposing previous panel before attaching new one")
|
||||
this.panel.dispose()
|
||||
this.panel = undefined
|
||||
}
|
||||
this.panel = ctx
|
||||
|
||||
this.stateReady = this.initializeState()
|
||||
@@ -135,11 +148,15 @@ export class AgentManagerProvider implements Disposable {
|
||||
this.sendKeybindings()
|
||||
|
||||
ctx.onDidDispose(() => {
|
||||
this.log("Panel disposed")
|
||||
this.statsPoller.stop()
|
||||
this.stopDiffPolling()
|
||||
// Only clear if this is still the active panel — a newer panel may
|
||||
// have already replaced us via attachPanel.
|
||||
if (this.panel === ctx) {
|
||||
this.log("Panel disposed")
|
||||
this.statsPoller.stop()
|
||||
this.stopDiffPolling()
|
||||
this.panel = undefined
|
||||
}
|
||||
ctx.sessions.dispose()
|
||||
this.panel = undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -191,6 +208,17 @@ export class AgentManagerProvider implements Disposable {
|
||||
if (m.type === "agentManager.deleteWorktree") return this.onDeleteWorktree(m.worktreeId)
|
||||
if (m.type === "agentManager.removeStaleWorktree") return this.onRemoveStaleWorktree(m.worktreeId)
|
||||
if (m.type === "agentManager.promoteSession") return this.onPromoteSession(m.sessionId)
|
||||
if (m.type === "agentManager.openLocally") {
|
||||
if (!this.panel) return null
|
||||
this.panel.sessions.clearSessionDirectory(m.sessionId)
|
||||
return null
|
||||
}
|
||||
if (m.type === "continueInWorktree") {
|
||||
void this.continueFromSidebar(m.sessionId, (status, detail, error) => {
|
||||
this.panel?.postMessage({ type: "continueInWorktreeProgress", status, detail, error })
|
||||
})
|
||||
return null
|
||||
}
|
||||
if (m.type === "agentManager.addSessionToWorktree") return this.onAddSessionToWorktree(m.worktreeId)
|
||||
if (m.type === "agentManager.forkSession") return this.onForkSession(m.sessionId, m.worktreeId)
|
||||
if (m.type === "agentManager.closeSession") return this.onCloseSession(m.sessionId)
|
||||
@@ -1306,10 +1334,14 @@ export class AgentManagerProvider implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
/** Run the worktree setup script if configured. Blocks until complete. Shows progress in overlay. */
|
||||
/** Copy .env files and run the worktree setup script. Blocks until complete. Shows progress in overlay. */
|
||||
private async runSetupScriptForWorktree(worktreePath: string, branch?: string, worktreeId?: string): Promise<void> {
|
||||
const root = this.getRoot()
|
||||
if (!root) return
|
||||
|
||||
// Always copy .env files from the main repo (before the setup script so it can override)
|
||||
await copyEnvFiles(root, worktreePath, (msg) => this.outputChannel.appendLine(`[EnvCopy] ${msg}`))
|
||||
|
||||
try {
|
||||
const service = this.getSetupScriptService()
|
||||
if (!service || !service.hasScript()) return
|
||||
@@ -1790,6 +1822,7 @@ export class AgentManagerProvider implements Disposable {
|
||||
public focusPanel(): void {
|
||||
if (!this.panel) return
|
||||
this.panel.reveal(false)
|
||||
this.postToWebview({ type: "action", action: "focusInput" })
|
||||
}
|
||||
|
||||
public isActive(): boolean {
|
||||
@@ -1801,6 +1834,42 @@ export class AgentManagerProvider implements Disposable {
|
||||
return this.panel?.sessions.getSessionDirectories() ?? new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue a sidebar session in a new worktree.
|
||||
* Captures git state, creates worktree, applies state, forks session.
|
||||
* Called from KiloProvider when the sidebar sends "continueInWorktree".
|
||||
*/
|
||||
public async continueFromSidebar(
|
||||
sessionId: string,
|
||||
progress: (status: string, detail?: string, error?: string) => void,
|
||||
): Promise<void> {
|
||||
const root = this.getRoot()
|
||||
if (!root) {
|
||||
progress("error", undefined, "No workspace folder open")
|
||||
return
|
||||
}
|
||||
|
||||
this.openPanel()
|
||||
await this.waitForStateReady("continueFromSidebar")
|
||||
|
||||
await continueInWorktree(
|
||||
{
|
||||
root,
|
||||
getClient: () => this.connectionService.getClient(),
|
||||
createWorktreeOnDisk: (opts) => this.createWorktreeOnDisk(opts),
|
||||
runSetupScript: (p, b, id) => this.runSetupScriptForWorktree(p, b, id),
|
||||
getStateManager: () => this.getStateManager(),
|
||||
registerWorktreeSession: (sid, dir) => this.registerWorktreeSession(sid, dir),
|
||||
registerSession: (session) => this.panel?.sessions.registerSession(session),
|
||||
notifyReady: (sid, result, wid) => this.notifyWorktreeReady(sid, result, wid),
|
||||
capture: (event, props) => this.host.capture(event, props),
|
||||
log: (...args) => this.log(...args),
|
||||
},
|
||||
sessionId,
|
||||
progress,
|
||||
)
|
||||
}
|
||||
|
||||
public postMessage(message: unknown): void {
|
||||
this.panel?.postMessage(message)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as nodePath from "path"
|
||||
import * as os from "os"
|
||||
import * as cp from "child_process"
|
||||
import * as fs from "fs/promises"
|
||||
import { spawn } from "../util/process"
|
||||
import simpleGit from "simple-git"
|
||||
import { parseWorktreeList, normalizePath } from "./git-import"
|
||||
|
||||
@@ -349,26 +349,11 @@ export class GitOps {
|
||||
|
||||
private exec(args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {
|
||||
return new Promise((resolve) => {
|
||||
const child = cp.execFile(
|
||||
"git",
|
||||
args,
|
||||
{
|
||||
cwd,
|
||||
env: options?.env,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (!error) {
|
||||
resolve({ code: 0, stdout, stderr })
|
||||
return
|
||||
}
|
||||
const exec = error as cp.ExecException
|
||||
const code = typeof exec.code === "number" ? exec.code : 1
|
||||
const fallback = exec.message || "Git command failed"
|
||||
resolve({ code, stdout: stdout ?? "", stderr: stderr || fallback })
|
||||
},
|
||||
)
|
||||
const child = spawn("git", args, {
|
||||
cwd,
|
||||
env: options?.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
})
|
||||
|
||||
if (options?.stdin !== undefined) {
|
||||
if (!child.stdin) {
|
||||
@@ -377,6 +362,22 @@ export class GitOps {
|
||||
}
|
||||
child.stdin.end(options.stdin)
|
||||
}
|
||||
|
||||
const out: Buffer[] = []
|
||||
const err: Buffer[] = []
|
||||
child.stdout?.on("data", (chunk: Buffer) => out.push(chunk))
|
||||
child.stderr?.on("data", (chunk: Buffer) => err.push(chunk))
|
||||
|
||||
child.on("error", (error) => {
|
||||
resolve({ code: 1, stdout: "", stderr: error.message })
|
||||
})
|
||||
child.on("close", (code) => {
|
||||
resolve({
|
||||
code: code ?? 1,
|
||||
stdout: Buffer.concat(out).toString("utf8"),
|
||||
stderr: Buffer.concat(err).toString("utf8"),
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { KiloClient, Session } from "@kilocode/sdk/v2/client"
|
||||
import type { CreateWorktreeResult } from "./WorktreeManager"
|
||||
import type { WorktreeStateManager } from "./WorktreeStateManager"
|
||||
import { capture as captureGitState, apply as applyGitState, type GitSnapshot } from "./git-transfer"
|
||||
import { getErrorMessage } from "../kilo-provider-utils"
|
||||
import { PLATFORM } from "./constants"
|
||||
|
||||
export interface ContinueContext {
|
||||
root: string
|
||||
getClient: () => KiloClient
|
||||
createWorktreeOnDisk: (opts: { baseBranch: string }) => Promise<{
|
||||
worktree: { id: string }
|
||||
result: CreateWorktreeResult
|
||||
} | null>
|
||||
runSetupScript: (path: string, branch: string, worktreeId: string) => Promise<void>
|
||||
getStateManager: () => WorktreeStateManager | undefined
|
||||
registerWorktreeSession: (sessionId: string, directory: string) => void
|
||||
registerSession: (session: Session) => void
|
||||
notifyReady: (sessionId: string, result: CreateWorktreeResult, worktreeId: string) => void
|
||||
capture: (event: string, props: Record<string, unknown>) => void
|
||||
log: (...args: unknown[]) => void
|
||||
}
|
||||
|
||||
/** Result type for each step — either success with a value or an error string. */
|
||||
export type StepResult<T> = { ok: true; value: T } | { ok: false; error: string }
|
||||
|
||||
/** Abort a running session. Best-effort — failures are logged but not fatal. */
|
||||
export async function abortSession(ctx: ContinueContext, sessionId: string): Promise<void> {
|
||||
try {
|
||||
const client = ctx.getClient()
|
||||
await client.session.abort({ sessionID: sessionId }).catch((err) => {
|
||||
ctx.log("Session abort failed (may already be idle):", getErrorMessage(err))
|
||||
})
|
||||
} catch (err) {
|
||||
ctx.log("Client not available for abort, continuing:", getErrorMessage(err))
|
||||
}
|
||||
}
|
||||
|
||||
/** Capture git state from the workspace root. */
|
||||
export async function captureState(ctx: ContinueContext): Promise<StepResult<GitSnapshot>> {
|
||||
try {
|
||||
const snapshot = await captureGitState(ctx.root, (...args) => ctx.log(...args))
|
||||
return { ok: true, value: snapshot }
|
||||
} catch (err) {
|
||||
return { ok: false, error: `Failed to capture git state: ${getErrorMessage(err)}` }
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a worktree and run the setup script. */
|
||||
export async function prepareWorktree(
|
||||
ctx: ContinueContext,
|
||||
branch: string,
|
||||
): Promise<StepResult<{ worktreeId: string; result: CreateWorktreeResult }>> {
|
||||
const created = await ctx.createWorktreeOnDisk({ baseBranch: branch })
|
||||
if (!created) return { ok: false, error: "Failed to create worktree" }
|
||||
await ctx.runSetupScript(created.result.path, created.result.branch, created.worktree.id)
|
||||
return { ok: true, value: { worktreeId: created.worktree.id, result: created.result } }
|
||||
}
|
||||
|
||||
/** Apply a git snapshot to a worktree directory. */
|
||||
export async function transferState(
|
||||
ctx: ContinueContext,
|
||||
snapshot: GitSnapshot,
|
||||
target: string,
|
||||
): Promise<StepResult<void>> {
|
||||
const applied = await applyGitState(snapshot, target, (...args) => ctx.log(...args))
|
||||
if (!applied.ok) {
|
||||
ctx.log("Git state transfer failed:", applied.error)
|
||||
return { ok: false, error: applied.error ?? "Failed to apply changes to worktree" }
|
||||
}
|
||||
return { ok: true, value: undefined }
|
||||
}
|
||||
|
||||
/** Fork the session into the worktree directory. */
|
||||
export async function forkSession(ctx: ContinueContext, sessionId: string, dir: string): Promise<StepResult<Session>> {
|
||||
let client: KiloClient
|
||||
try {
|
||||
client = ctx.getClient()
|
||||
} catch (err) {
|
||||
ctx.log("Client not available for session fork:", getErrorMessage(err))
|
||||
return { ok: false, error: "Not connected to CLI backend" }
|
||||
}
|
||||
try {
|
||||
const { data } = await client.session.fork({ sessionID: sessionId, directory: dir }, { throwOnError: true })
|
||||
return { ok: true, value: data }
|
||||
} catch (err) {
|
||||
return { ok: false, error: `Failed to fork session: ${getErrorMessage(err)}` }
|
||||
}
|
||||
}
|
||||
|
||||
/** Register the forked session in state and emit telemetry. */
|
||||
export function registerSession(
|
||||
ctx: ContinueContext,
|
||||
session: Session,
|
||||
result: CreateWorktreeResult,
|
||||
worktreeId: string,
|
||||
sourceId: string,
|
||||
): void {
|
||||
const state = ctx.getStateManager()
|
||||
if (state) state.addSession(session.id, worktreeId)
|
||||
ctx.registerWorktreeSession(session.id, result.path)
|
||||
ctx.registerSession(session)
|
||||
ctx.notifyReady(session.id, result, worktreeId)
|
||||
ctx.capture("Continue in Worktree", { source: PLATFORM, sessionId: session.id, worktreeId })
|
||||
ctx.log(`Continued sidebar session ${sourceId} → worktree ${worktreeId} (session ${session.id})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue a sidebar session in a new worktree.
|
||||
* Orchestrates the atomic steps: abort → capture → prepare → transfer → fork → register.
|
||||
*
|
||||
* Pure orchestration — no vscode imports.
|
||||
*/
|
||||
export async function continueInWorktree(
|
||||
ctx: ContinueContext,
|
||||
sessionId: string,
|
||||
progress: (status: string, detail?: string, error?: string) => void,
|
||||
): Promise<void> {
|
||||
await abortSession(ctx, sessionId)
|
||||
|
||||
progress("capturing", "Capturing git changes...")
|
||||
const captured = await captureState(ctx)
|
||||
if (!captured.ok) return progress("error", undefined, captured.error)
|
||||
|
||||
progress("creating", "Creating worktree...")
|
||||
const prepared = await prepareWorktree(ctx, captured.value.branch)
|
||||
if (!prepared.ok) return progress("error", undefined, prepared.error)
|
||||
|
||||
progress("transferring", "Transferring changes...")
|
||||
const transferred = await transferState(ctx, captured.value, prepared.value.result.path)
|
||||
if (!transferred.ok) return progress("error", undefined, transferred.error)
|
||||
|
||||
progress("forking", "Starting session...")
|
||||
const forked = await forkSession(ctx, sessionId, prepared.value.result.path)
|
||||
if (!forked.ok) return progress("error", undefined, forked.error)
|
||||
|
||||
registerSession(ctx, forked.value, prepared.value.result, prepared.value.worktreeId, sessionId)
|
||||
progress("done")
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copies .env files from the main repository into a worktree.
|
||||
*
|
||||
* Runs automatically before the user setup script so worktrees inherit
|
||||
* environment configuration without manual copying. Only plain files
|
||||
* named exactly `.env` or matching `.env.<qualifier>` (e.g. `.env.local`,
|
||||
* `.env.development`) at the repo root are considered. Files like `.envrc`
|
||||
* or `.environment` are excluded — they follow different conventions.
|
||||
* Nested directories are not traversed and existing files in the worktree
|
||||
* are never overwritten.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs"
|
||||
import { constants } from "node:fs"
|
||||
import * as path from "node:path"
|
||||
|
||||
/** Result returned after a copy attempt. */
|
||||
export interface EnvCopyResult {
|
||||
/** Files that were copied. */
|
||||
copied: string[]
|
||||
/** Files that were skipped because they already exist in the worktree. */
|
||||
skipped: string[]
|
||||
}
|
||||
|
||||
type Log = (msg: string) => void
|
||||
|
||||
/** Match `.env` exactly, or `.env.` followed by a qualifier. */
|
||||
function isEnvFile(name: string): boolean {
|
||||
return name === ".env" || name.startsWith(".env.")
|
||||
}
|
||||
|
||||
/**
|
||||
* List `.env` / `.env.*` filenames at the root of `dir`.
|
||||
* Returns basenames only (e.g. `[".env", ".env.local"]`).
|
||||
*/
|
||||
export function listEnvFiles(dir: string): string[] {
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
return entries.filter((e) => e.isFile() && isEnvFile(e.name)).map((e) => e.name)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy `.env` / `.env.*` files from `repoPath` into `worktreePath`.
|
||||
*
|
||||
* - Only root-level files named `.env` or `.env.<qualifier>` are copied.
|
||||
* - Files that already exist in the worktree are skipped (no overwrite).
|
||||
* - Cross-platform: uses only Node `fs` APIs.
|
||||
*/
|
||||
export async function copyEnvFiles(
|
||||
repoPath: string,
|
||||
worktreePath: string,
|
||||
log: Log = () => {},
|
||||
): Promise<EnvCopyResult> {
|
||||
const names = listEnvFiles(repoPath)
|
||||
if (names.length === 0) {
|
||||
log("No .env files found in main repo")
|
||||
return { copied: [], skipped: [] }
|
||||
}
|
||||
|
||||
const result: EnvCopyResult = { copied: [], skipped: [] }
|
||||
|
||||
for (const name of names) {
|
||||
const src = path.join(repoPath, name)
|
||||
const dst = path.join(worktreePath, name)
|
||||
|
||||
try {
|
||||
// COPYFILE_EXCL fails atomically if dst exists — no TOCTOU race.
|
||||
await fs.promises.copyFile(src, dst, constants.COPYFILE_EXCL)
|
||||
log(`Copied ${name}`)
|
||||
result.copied.push(name)
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && "code" in err && err.code === "EEXIST") {
|
||||
log(`Skipping ${name} (already exists in worktree)`)
|
||||
result.skipped.push(name)
|
||||
continue
|
||||
}
|
||||
log(`Failed to copy ${name}: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import * as nodePath from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import * as cp from "child_process"
|
||||
|
||||
/**
|
||||
* Portable git state snapshot — captures uncommitted changes as patches
|
||||
* that can be applied to any directory on the same commit.
|
||||
*
|
||||
* Used by "Continue in Worktree" to copy git state from the user's
|
||||
* working tree into a fresh worktree without modifying the source.
|
||||
*/
|
||||
export interface GitSnapshot {
|
||||
branch: string
|
||||
head: string
|
||||
/** Binary-safe unified diff of unstaged changes, or null if clean. */
|
||||
unstaged: string | null
|
||||
/** Binary-safe unified diff of staged changes, or null if none staged. */
|
||||
staged: string | null
|
||||
/** Untracked files (new files not yet added to git). */
|
||||
untracked: UntrackedFile[]
|
||||
}
|
||||
|
||||
export interface UntrackedFile {
|
||||
/** Relative path from repo root. */
|
||||
path: string
|
||||
/** Raw file content. */
|
||||
content: Buffer
|
||||
}
|
||||
|
||||
const MAX_FILE = 10 * 1024 * 1024 // 10 MB
|
||||
|
||||
function git(args: string[], cwd: string, stdin?: string): Promise<{ code: number; stdout: string; stderr: string }> {
|
||||
return new Promise((resolve) => {
|
||||
if (stdin !== undefined) {
|
||||
// Use spawn for stdin piping — execFile doesn't reliably create a stdin pipe
|
||||
const child = cp.spawn("git", args, { cwd, windowsHide: true })
|
||||
let stdout = ""
|
||||
let stderr = ""
|
||||
child.stdout.on("data", (d: Buffer) => (stdout += d.toString()))
|
||||
child.stderr.on("data", (d: Buffer) => (stderr += d.toString()))
|
||||
child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }))
|
||||
child.stdin.end(stdin)
|
||||
} else {
|
||||
cp.execFile(
|
||||
"git",
|
||||
args,
|
||||
{ cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, windowsHide: true },
|
||||
(error, stdout, stderr) => {
|
||||
if (!error) {
|
||||
resolve({ code: 0, stdout, stderr })
|
||||
return
|
||||
}
|
||||
const exec = error as cp.ExecException
|
||||
resolve({ code: typeof exec.code === "number" ? exec.code : 1, stdout: stdout ?? "", stderr: stderr ?? "" })
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function raw(args: string[], cwd: string): Promise<string> {
|
||||
const result = await git(args, cwd)
|
||||
return result.stdout.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture the current git state from `cwd` as a portable snapshot.
|
||||
* This is a read-only operation — the source directory is never modified.
|
||||
*/
|
||||
export async function capture(cwd: string, log: (...args: unknown[]) => void): Promise<GitSnapshot> {
|
||||
const patch = (args: string[]) =>
|
||||
git(args, cwd).then((r) => {
|
||||
const out = r.stdout
|
||||
return out.trim() ? out : null
|
||||
})
|
||||
|
||||
const [branch, head, unstaged, staged, untrackedRaw] = await Promise.all([
|
||||
raw(["branch", "--show-current"], cwd),
|
||||
raw(["rev-parse", "HEAD"], cwd),
|
||||
patch(["diff", "--binary"]),
|
||||
patch(["diff", "--cached", "--binary"]),
|
||||
raw(["ls-files", "--others", "--exclude-standard"], cwd).then((s: string) =>
|
||||
s.split("\n").filter((l: string) => l.length > 0),
|
||||
),
|
||||
])
|
||||
|
||||
const untracked: UntrackedFile[] = []
|
||||
for (const rel of untrackedRaw) {
|
||||
const full = nodePath.resolve(cwd, rel)
|
||||
try {
|
||||
const stat = await fs.stat(full)
|
||||
if (stat.size > MAX_FILE) {
|
||||
log(`Skipping untracked file ${rel}: ${(stat.size / 1024 / 1024).toFixed(1)} MB exceeds limit`)
|
||||
continue
|
||||
}
|
||||
const content = await fs.readFile(full)
|
||||
untracked.push({ path: rel, content })
|
||||
} catch (err) {
|
||||
log(`Failed to read untracked file ${rel}:`, err)
|
||||
}
|
||||
}
|
||||
|
||||
return { branch, head, unstaged, staged, untracked }
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a git snapshot to a target directory.
|
||||
* Applies staged changes (and re-stages them), unstaged changes, and writes untracked files.
|
||||
*/
|
||||
export async function apply(
|
||||
snapshot: GitSnapshot,
|
||||
target: string,
|
||||
log: (...args: unknown[]) => void,
|
||||
): Promise<{ ok: boolean; error?: string }> {
|
||||
// Apply staged patch first, then re-stage those files
|
||||
if (snapshot.staged) {
|
||||
const result = await git(["apply", "--whitespace=nowarn", "-"], target, snapshot.staged)
|
||||
if (result.code !== 0) {
|
||||
const msg = result.stderr.trim() || "Patch did not apply"
|
||||
log("Failed to apply staged patch:", msg)
|
||||
return { ok: false, error: `Staged patch failed: ${msg}` }
|
||||
}
|
||||
const files = parsePatchFiles(snapshot.staged)
|
||||
if (files.length > 0) {
|
||||
await git(["add", "--", ...files], target)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply unstaged patch (leave as unstaged working-tree changes)
|
||||
if (snapshot.unstaged) {
|
||||
const result = await git(["apply", "--whitespace=nowarn", "-"], target, snapshot.unstaged)
|
||||
if (result.code !== 0) {
|
||||
const msg = result.stderr.trim() || "Patch did not apply"
|
||||
log("Failed to apply unstaged patch:", msg)
|
||||
return { ok: false, error: `Unstaged patch failed: ${msg}` }
|
||||
}
|
||||
}
|
||||
|
||||
// Write untracked files
|
||||
for (const file of snapshot.untracked) {
|
||||
const full = nodePath.resolve(target, file.path)
|
||||
try {
|
||||
await fs.mkdir(nodePath.dirname(full), { recursive: true })
|
||||
await fs.writeFile(full, file.content)
|
||||
} catch (err) {
|
||||
log(`Failed to write untracked file ${file.path}:`, err)
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
/** Extract file paths from a unified diff's `diff --git a/... b/...` headers. */
|
||||
function parsePatchFiles(patch: string): string[] {
|
||||
const files: string[] = []
|
||||
for (const line of patch.split("\n")) {
|
||||
const match = /^diff --git a\/.+ b\/(.+)$/.exec(line)
|
||||
if (match && match[1]) files.push(match[1])
|
||||
}
|
||||
return files
|
||||
}
|
||||
@@ -11,11 +11,9 @@
|
||||
* process.env.PATH so all subsequent child_process calls benefit.
|
||||
*/
|
||||
|
||||
import { type ExecFileOptionsWithStringEncoding, execFile } from "child_process"
|
||||
import { type ExecFileOptionsWithStringEncoding } from "child_process"
|
||||
import * as os from "os"
|
||||
import { promisify } from "util"
|
||||
|
||||
const run = promisify(execFile)
|
||||
import { exec } from "../util/process"
|
||||
|
||||
// Environment variable keys match: letters, digits, underscores, starting with a non-digit.
|
||||
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*=/
|
||||
@@ -70,7 +68,7 @@ export async function getShellEnvironment(): Promise<Record<string, string>> {
|
||||
const shell = process.env.SHELL || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash")
|
||||
|
||||
try {
|
||||
const { stdout } = await run(shell, ["-lc", "env"], {
|
||||
const { stdout } = await exec(shell, ["-lc", "env"], {
|
||||
timeout: 10_000,
|
||||
env: { ...process.env, HOME: os.homedir() },
|
||||
})
|
||||
@@ -127,7 +125,7 @@ export async function execWithShellEnv(
|
||||
options?: Omit<ExecFileOptionsWithStringEncoding, "encoding">,
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
try {
|
||||
return await run(cmd, args, { ...options, encoding: "utf8" })
|
||||
return await exec(cmd, args, options)
|
||||
} catch (error) {
|
||||
if (
|
||||
process.platform !== "darwin" ||
|
||||
@@ -141,13 +139,13 @@ export async function execWithShellEnv(
|
||||
// Already resolved and PATH was actually changed — no point retrying resolution.
|
||||
// Just retry with the (already-patched) process.env.
|
||||
if (fixed) {
|
||||
return await run(cmd, args, { ...options, encoding: "utf8" })
|
||||
return await exec(cmd, args, options)
|
||||
}
|
||||
|
||||
// If another caller is already resolving, wait for it then retry.
|
||||
if (fixing) {
|
||||
await fixing
|
||||
return await run(cmd, args, { ...options, encoding: "utf8" })
|
||||
return await exec(cmd, args, options)
|
||||
}
|
||||
|
||||
console.log(`[shell-env] "${cmd}" not found, resolving shell environment`)
|
||||
@@ -159,7 +157,7 @@ export async function execWithShellEnv(
|
||||
fixing = null
|
||||
}
|
||||
|
||||
return await run(cmd, args, { ...options, encoding: "utf8" })
|
||||
return await exec(cmd, args, options)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -229,6 +229,11 @@ interface PromoteSessionIn {
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
interface OpenLocallyIn {
|
||||
type: "agentManager.openLocally"
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
interface AddSessionToWorktreeIn {
|
||||
type: "agentManager.addSessionToWorktree"
|
||||
worktreeId: string
|
||||
@@ -411,12 +416,18 @@ interface AbortIn {
|
||||
sessionID: string
|
||||
}
|
||||
|
||||
interface ContinueInWorktreeIn {
|
||||
type: "continueInWorktree"
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
/** All messages the Agent Manager expects from the webview (onMessage input). */
|
||||
export type AgentManagerInMessage =
|
||||
| CreateWorktreeIn
|
||||
| DeleteWorktreeIn
|
||||
| RemoveStaleWorktreeIn
|
||||
| PromoteSessionIn
|
||||
| OpenLocallyIn
|
||||
| AddSessionToWorktreeIn
|
||||
| CloseSessionIn
|
||||
| ForkSessionIn
|
||||
@@ -451,3 +462,4 @@ export type AgentManagerInMessage =
|
||||
| LoadMessagesIn
|
||||
| ClearSessionIn
|
||||
| AbortIn
|
||||
| ContinueInWorktreeIn
|
||||
|
||||
@@ -56,11 +56,22 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
// Ensure Agent Manager keybindings work when a VS Code terminal has focus.
|
||||
// The terminal intercepts all keystrokes unless the command is listed in
|
||||
// terminal.integrated.commandsToSkipShell, which only contains built-in
|
||||
// commands by default.
|
||||
ensureCommandsSkipShell(["kilo-code.new.agentManagerOpen", "kilo-code.new.agentManager.showTerminal"])
|
||||
|
||||
// Create Agent Manager provider for editor panel
|
||||
const agentManagerHost = new VscodeHost(context.extensionUri, connectionService, context)
|
||||
const agentManagerProvider = new AgentManagerProvider(agentManagerHost, connectionService)
|
||||
context.subscriptions.push(agentManagerProvider)
|
||||
|
||||
// Wire "Continue in Worktree" from sidebar → Agent Manager
|
||||
provider.setContinueInWorktreeHandler((sessionId, progress) =>
|
||||
agentManagerProvider.continueFromSidebar(sessionId, progress),
|
||||
)
|
||||
|
||||
// Register serializer so Agent Manager restores when VS Code restarts
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewPanelSerializer(AgentManagerProvider.viewType, {
|
||||
@@ -155,9 +166,6 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
vscode.commands.registerCommand("kilo-code.new.historyButtonClicked", () => {
|
||||
provider.postMessage({ type: "action", action: "historyButtonClicked" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.cloudHistoryButtonClicked", () => {
|
||||
provider.postMessage({ type: "action", action: "cloudHistoryButtonClicked" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.cycleAgentMode", () => {
|
||||
provider.postMessage({ type: "action", action: "cycleAgentMode" })
|
||||
agentManagerProvider.postMessage({ type: "action", action: "cycleAgentMode" })
|
||||
@@ -354,6 +362,26 @@ async function openKiloInNewTab(context: vscode.ExtensionContext, connectionServ
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add extension commands to terminal.integrated.commandsToSkipShell so they
|
||||
* work when a VS Code terminal has focus. The setting only ships with built-in
|
||||
* commands; extension commands must be added explicitly.
|
||||
*/
|
||||
function ensureCommandsSkipShell(commands: string[]): void {
|
||||
const config = vscode.workspace.getConfiguration("terminal.integrated")
|
||||
const info = config.inspect<string[]>("commandsToSkipShell")
|
||||
// Update whichever scope already carries an override so we don't
|
||||
// shadow workspace settings or leak workspace values into global.
|
||||
const [existing, target] = info?.workspaceFolderValue
|
||||
? [info.workspaceFolderValue, vscode.ConfigurationTarget.WorkspaceFolder]
|
||||
: info?.workspaceValue
|
||||
? [info.workspaceValue, vscode.ConfigurationTarget.Workspace]
|
||||
: [info?.globalValue ?? [], vscode.ConfigurationTarget.Global]
|
||||
const missing = commands.filter((cmd) => !existing.includes(cmd))
|
||||
if (missing.length === 0) return
|
||||
config.update("commandsToSkipShell", [...existing, ...missing], target)
|
||||
}
|
||||
|
||||
function waitForWebviewPanelToBeActive(panel: vscode.WebviewPanel): Promise<void> {
|
||||
if (panel.active) {
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -167,6 +167,32 @@ export function buildSettingPath(key: string): { section: string; leaf: string }
|
||||
return { section, leaf }
|
||||
}
|
||||
|
||||
export function resolveWorkspaceDirectory(input: {
|
||||
sessionID?: string
|
||||
sessionDirectories: Map<string, string>
|
||||
workspaceDirectory: string
|
||||
}) {
|
||||
if (!input.sessionID) return input.workspaceDirectory
|
||||
|
||||
const dir = input.sessionDirectories.get(input.sessionID)
|
||||
if (dir) return dir
|
||||
|
||||
return input.workspaceDirectory
|
||||
}
|
||||
|
||||
export function resolveContextDirectory(input: {
|
||||
currentSessionID?: string
|
||||
contextSessionID?: string
|
||||
sessionDirectories: Map<string, string>
|
||||
workspaceDirectory: string
|
||||
}) {
|
||||
return resolveWorkspaceDirectory({
|
||||
sessionID: input.currentSessionID ?? input.contextSessionID,
|
||||
sessionDirectories: input.sessionDirectories,
|
||||
workspaceDirectory: input.workspaceDirectory,
|
||||
})
|
||||
}
|
||||
|
||||
export type WebviewMessage =
|
||||
| {
|
||||
type: "partUpdated"
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface AuthContext {
|
||||
getWorkspaceDirectory(): string
|
||||
disposeGlobal(): Promise<void>
|
||||
fetchAndSendProviders(): Promise<void>
|
||||
fetchAndSendAgents(): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,6 +132,11 @@ export async function handleSetOrganization(ctx: AuthContext, organizationId: st
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to refresh providers after org switch:", error)
|
||||
}
|
||||
try {
|
||||
await ctx.fetchAndSendAgents()
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to refresh agents after org switch:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle profile refresh request. */
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
/** Strip a filediff down to path + addition/deletion counts. */
|
||||
function slimMeta(meta: unknown): Record<string, unknown> | undefined {
|
||||
function slimEditMeta(meta: unknown): Record<string, unknown> | undefined {
|
||||
if (!meta || typeof meta !== "object") return undefined
|
||||
|
||||
const obj = meta as Record<string, unknown>
|
||||
@@ -30,7 +30,7 @@ function slimMeta(meta: unknown): Record<string, unknown> | undefined {
|
||||
return result
|
||||
}
|
||||
|
||||
/** Strip heavy metadata from a single edit tool part; pass-through for all other part types. */
|
||||
/** Strip heavy metadata from a single edit tool part; pass-through for everything else. */
|
||||
export function slimPart<T>(part: T): T {
|
||||
if (!part || typeof part !== "object") return part
|
||||
|
||||
@@ -41,7 +41,7 @@ export function slimPart<T>(part: T): T {
|
||||
if (!state || typeof state !== "object") return part
|
||||
|
||||
const next = { ...(state as Record<string, unknown>) }
|
||||
const meta = slimMeta(next.metadata)
|
||||
const meta = slimEditMeta(next.metadata)
|
||||
if (meta) next.metadata = meta
|
||||
else delete next.metadata
|
||||
|
||||
|
||||
@@ -65,6 +65,10 @@ export class AutocompleteModel {
|
||||
let inputTokens = 0
|
||||
let outputTokens = 0
|
||||
|
||||
// Capture SSE-level errors so they propagate to the caller. The SDK's SSE
|
||||
// client catches HTTP errors (402, 401, 429, 5xx) internally and silently
|
||||
// ends the stream. Without this, errors never reach ErrorBackoff.
|
||||
let sseError: Error | undefined
|
||||
const { stream } = await client.kilo.fim(
|
||||
{
|
||||
prefix,
|
||||
@@ -73,7 +77,13 @@ export class AutocompleteModel {
|
||||
maxTokens: 256,
|
||||
temperature: 0.2,
|
||||
},
|
||||
{ signal },
|
||||
{
|
||||
signal,
|
||||
sseMaxRetryAttempts: 1,
|
||||
onSseError: (error) => {
|
||||
sseError = error instanceof Error ? error : new Error(String(error))
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
@@ -86,6 +96,8 @@ export class AutocompleteModel {
|
||||
if (chunk.cost !== undefined) cost = chunk.cost
|
||||
}
|
||||
|
||||
if (sseError) throw sseError
|
||||
|
||||
return {
|
||||
cost,
|
||||
inputTokens,
|
||||
@@ -127,4 +139,20 @@ export class AutocompleteModel {
|
||||
}
|
||||
return this.connectionService.getConnectionState() === "connected"
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the user's credit balance via the profile endpoint.
|
||||
* Returns true if the user has a positive balance, false otherwise.
|
||||
* Returns false on any error (not connected, fetch failed, etc.).
|
||||
*/
|
||||
public async hasBalance(): Promise<boolean> {
|
||||
if (!this.connectionService || this.connectionService.getConnectionState() !== "connected") {
|
||||
return false
|
||||
}
|
||||
const result = await this.connectionService
|
||||
.getClient()
|
||||
.kilo.profile()
|
||||
.catch(() => null)
|
||||
return (result?.data?.balance?.balance ?? 0) > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ export class AutocompleteServiceManager {
|
||||
public readonly inlineCompletionProvider: AutocompleteInlineCompletionProvider
|
||||
private inlineCompletionProviderDisposable: vscode.Disposable | null = null
|
||||
private unsubscribeState: (() => void) | null = null
|
||||
private unsubscribeEvent: (() => void) | null = null
|
||||
|
||||
constructor(context: vscode.ExtensionContext, connectionService: KiloConnectionService) {
|
||||
if (AutocompleteServiceManager._instance) {
|
||||
@@ -84,14 +85,26 @@ export class AutocompleteServiceManager {
|
||||
() => this.settings,
|
||||
workspacePath,
|
||||
new AutocompleteTelemetry(),
|
||||
(status) => this.handleFatalAutocompleteError(status),
|
||||
)
|
||||
|
||||
// Reload when CLI backend connection state changes so autocomplete
|
||||
// picks up the connected state even if it wasn't ready at startup.
|
||||
// Also reset error backoff — a reconnect may mean the user re-authenticated
|
||||
// or added credits, so we should give autocomplete a fresh chance.
|
||||
this.unsubscribeState = connectionService.onStateChange(() => {
|
||||
this.inlineCompletionProvider.resetBackoff()
|
||||
void this.load()
|
||||
})
|
||||
|
||||
// Reset error backoff when auth state changes (login, logout, org switch).
|
||||
// The CLI emits global.disposed after these actions, which is the most
|
||||
// reliable signal that credentials may have changed.
|
||||
this.unsubscribeEvent = connectionService.onEventFiltered(
|
||||
(event) => event.type === "global.disposed",
|
||||
() => this.inlineCompletionProvider.resetBackoff(),
|
||||
)
|
||||
|
||||
void this.load()
|
||||
}
|
||||
|
||||
@@ -312,6 +325,27 @@ export class AutocompleteServiceManager {
|
||||
return !this.model.hasValidCredentials()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a fatal (non-retriable) autocomplete error such as 402 Payment Required.
|
||||
* Shows a one-time notification to the user so they know autocomplete is paused.
|
||||
*/
|
||||
private handleFatalAutocompleteError(status: number | null): void {
|
||||
const msg =
|
||||
status === 402
|
||||
? t("kilocode:autocomplete.creditsExhausted.message")
|
||||
: t("kilocode:autocomplete.authError.message")
|
||||
|
||||
if (status === 402) {
|
||||
vscode.window.showWarningMessage(msg, t("kilocode:autocomplete.creditsExhausted.addCredits")).then((choice) => {
|
||||
if (choice === t("kilocode:autocomplete.creditsExhausted.addCredits")) {
|
||||
vscode.env.openExternal(vscode.Uri.parse("https://app.kilo.ai/credits"))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
vscode.window.showWarningMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
private updateCostTracking(cost: number, _inputTokens: number, _outputTokens: number): void {
|
||||
this.completionCount++
|
||||
this.sessionCost += cost
|
||||
@@ -360,9 +394,11 @@ export class AutocompleteServiceManager {
|
||||
this.snoozeTimer = null
|
||||
}
|
||||
|
||||
// Unsubscribe from connection state changes
|
||||
// Unsubscribe from connection state changes and SSE events
|
||||
this.unsubscribeState?.()
|
||||
this.unsubscribeState = null
|
||||
this.unsubscribeEvent?.()
|
||||
this.unsubscribeEvent = null
|
||||
|
||||
// Dispose inline completion provider registration
|
||||
if (this.inlineCompletionProviderDisposable) {
|
||||
|
||||
+59
-15
@@ -22,7 +22,6 @@ import {
|
||||
calcDebounceDelay,
|
||||
MatchingSuggestionWithFillIn as _MatchingSuggestionWithFillIn,
|
||||
} from "./inline-utils"
|
||||
import { HoleFiller } from "./HoleFiller"
|
||||
import { FimPromptBuilder } from "./FillInTheMiddle"
|
||||
import { AutocompleteModel } from "../AutocompleteModel"
|
||||
import { ContextRetrievalService } from "../continuedev/core/autocomplete/context/ContextRetrievalService"
|
||||
@@ -34,6 +33,7 @@ import { postprocessAutocompleteSuggestion } from "./uselessSuggestionFilter"
|
||||
import { shouldSkipAutocomplete } from "./contextualSkip"
|
||||
import { FileIgnoreController } from "../shims/FileIgnoreController"
|
||||
import { AutocompleteTelemetry } from "./AutocompleteTelemetry"
|
||||
import { ErrorBackoff } from "./ErrorBackoff"
|
||||
|
||||
const MAX_SUGGESTIONS_HISTORY = 20
|
||||
|
||||
@@ -119,7 +119,6 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple
|
||||
public suggestionsHistory: FillInAtCursorSuggestion[] = []
|
||||
/** Tracks all pending/in-flight requests */
|
||||
private pendingRequests: PendingRequest[] = []
|
||||
private holeFiller: HoleFiller
|
||||
private fimPromptBuilder: FimPromptBuilder
|
||||
private model: AutocompleteModel
|
||||
private costTrackingCallback: CostTrackingCallback
|
||||
@@ -137,6 +136,12 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple
|
||||
private telemetry: AutocompleteTelemetry | null
|
||||
/** Information about the last suggestion shown to the user */
|
||||
private lastSuggestion: LastSuggestionInfo | null = null
|
||||
/** Circuit breaker / exponential backoff for API errors */
|
||||
public readonly backoff = new ErrorBackoff()
|
||||
/** Optional callback fired once when a fatal (non-retriable) error is first detected */
|
||||
private onFatalError: ((status: number | null) => void) | null = null
|
||||
/** Whether the fatal error notification has already been fired (avoid repeating) */
|
||||
private fatalNotified = false
|
||||
|
||||
constructor(
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -145,11 +150,13 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple
|
||||
getSettings: () => AutocompleteServiceSettings | null,
|
||||
workspacePath: string,
|
||||
telemetry: AutocompleteTelemetry | null = null,
|
||||
onFatalError?: (status: number | null) => void,
|
||||
) {
|
||||
this.telemetry = telemetry
|
||||
this.model = model
|
||||
this.costTrackingCallback = costTrackingCallback
|
||||
this.getSettings = getSettings
|
||||
this.onFatalError = onFatalError ?? null
|
||||
|
||||
this.ignoreController = (async () => {
|
||||
const ignoreController = new FileIgnoreController(workspacePath)
|
||||
@@ -165,7 +172,6 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple
|
||||
model,
|
||||
ignoreController: this.ignoreController,
|
||||
}
|
||||
this.holeFiller = new HoleFiller(contextProvider)
|
||||
this.fimPromptBuilder = new FimPromptBuilder(contextProvider)
|
||||
|
||||
this.recentlyVisitedRangesService = new RecentlyVisitedRangesService(ide)
|
||||
@@ -218,10 +224,10 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple
|
||||
const { prefix, suffix } = extractPrefixSuffix(document, position)
|
||||
const languageId = document.languageId
|
||||
|
||||
// Determine strategy based on model capabilities and call only the appropriate prompt builder
|
||||
const prompt = this.model.supportsFim()
|
||||
? await this.fimPromptBuilder.getFimPrompts(autocompleteInput, this.model.getModelName() ?? "codestral")
|
||||
: await this.holeFiller.getPrompts(autocompleteInput, languageId)
|
||||
const prompt = await this.fimPromptBuilder.getFimPrompts(
|
||||
autocompleteInput,
|
||||
this.model.getModelName() ?? "codestral",
|
||||
)
|
||||
|
||||
return { prompt, prefix, suffix }
|
||||
}
|
||||
@@ -277,6 +283,15 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset error backoff and allow fatal notifications to fire again.
|
||||
* Call this when auth state changes (login, reconnect, org switch).
|
||||
*/
|
||||
public resetBackoff(): void {
|
||||
this.backoff.reset()
|
||||
this.fatalNotified = false
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.debounceTimer !== null) {
|
||||
clearTimeout(this.debounceTimer)
|
||||
@@ -333,6 +348,24 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple
|
||||
return []
|
||||
}
|
||||
|
||||
// Circuit breaker / backoff: skip requests when the API is returning errors.
|
||||
// This prevents flooding the API with thousands of failed requests when
|
||||
// credits are depleted (402), auth is invalid (401/403), or the server
|
||||
// is rate-limiting (429) / having issues (5xx).
|
||||
if (this.backoff.blocked()) {
|
||||
// For 402 (credits depleted), periodically check the balance endpoint
|
||||
// instead of sending a probe FIM request. If the user has added credits,
|
||||
// reset the backoff so autocomplete resumes.
|
||||
if (this.backoff.getFatalStatus() === 402 && this.backoff.shouldProbe()) {
|
||||
const funded = await this.model.hasBalance()
|
||||
if (funded) {
|
||||
this.backoff.reset()
|
||||
this.fatalNotified = false
|
||||
}
|
||||
}
|
||||
if (this.backoff.blocked()) return []
|
||||
}
|
||||
|
||||
if (!document?.uri?.fsPath) {
|
||||
return []
|
||||
}
|
||||
@@ -389,9 +422,6 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple
|
||||
|
||||
const { prompt, prefix: promptPrefix, suffix: promptSuffix } = await this.getPrompt(document, position)
|
||||
|
||||
// Update context with strategy now that we know it
|
||||
telemetryContext.strategy = prompt.strategy
|
||||
|
||||
await this.debouncedFetchAndCacheSuggestion(prompt, promptPrefix, promptSuffix, document.languageId)
|
||||
|
||||
const cachedResult = applyFirstLineOnly(findMatchingSuggestion(prefix, suffix, this.suggestionsHistory), prefix)
|
||||
@@ -525,7 +555,6 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple
|
||||
languageId,
|
||||
modelId: this.model?.getModelName(),
|
||||
provider: this.model?.getProviderDisplayName(),
|
||||
strategy: prompt.strategy,
|
||||
}
|
||||
|
||||
// Defense-in-depth: credentials may become invalid between the provider gate and the actual
|
||||
@@ -540,10 +569,12 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple
|
||||
const curriedProcessSuggestion = (text: string) =>
|
||||
this.processSuggestion(text, prefix, suffix, this.model, telemetryContext, languageId)
|
||||
|
||||
const result =
|
||||
prompt.strategy === "fim"
|
||||
? await this.fimPromptBuilder.getFromFIM(this.model, prompt, curriedProcessSuggestion, controller.signal)
|
||||
: await this.holeFiller.getFromChat(this.model, prompt, curriedProcessSuggestion)
|
||||
const result = await this.fimPromptBuilder.getFromFIM(
|
||||
this.model,
|
||||
prompt,
|
||||
curriedProcessSuggestion,
|
||||
controller.signal,
|
||||
)
|
||||
|
||||
const latencyMs = performance.now() - startTime
|
||||
|
||||
@@ -562,6 +593,10 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple
|
||||
|
||||
this.costTrackingCallback(result.cost, result.inputTokens, result.outputTokens)
|
||||
|
||||
// Successful response — reset any backoff / circuit breaker state
|
||||
this.backoff.success()
|
||||
this.fatalNotified = false
|
||||
|
||||
// Always update suggestions, even if text is empty (for caching)
|
||||
this.updateSuggestions(result.suggestion)
|
||||
} catch (error) {
|
||||
@@ -576,6 +611,15 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple
|
||||
},
|
||||
telemetryContext,
|
||||
)
|
||||
|
||||
// Update circuit breaker / backoff state based on the error kind
|
||||
const kind = this.backoff.failure(error)
|
||||
|
||||
// Notify once when a fatal error (402/401/403) is first detected
|
||||
if (kind === "fatal" && !this.fatalNotified) {
|
||||
this.fatalNotified = true
|
||||
this.onFatalError?.(this.backoff.getFatalStatus())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Circuit breaker and exponential backoff for autocomplete request errors.
|
||||
*
|
||||
* Classifies errors into:
|
||||
* - **fatal**: 401, 402, 403 — stops all requests until explicitly reset
|
||||
* - **retriable**: 429, 5xx, network errors — exponential backoff with cap
|
||||
* - **transient**: abort, unknown — no special handling
|
||||
*
|
||||
* When a fatal error (like 402 Payment Required) is detected, autocomplete
|
||||
* requests are blocked to prevent thousands of wasted API calls. The caller
|
||||
* can periodically check `shouldProbe()` to run a lightweight balance check
|
||||
* and call `reset()` if the user has added credits.
|
||||
*/
|
||||
|
||||
/** Base backoff delay in ms for retriable errors */
|
||||
const BASE_DELAY_MS = 2_000
|
||||
|
||||
/** Maximum backoff delay in ms (2 minutes) */
|
||||
const MAX_DELAY_MS = 120_000
|
||||
|
||||
/** Number of consecutive retriable failures before circuit opens */
|
||||
const CIRCUIT_THRESHOLD = 5
|
||||
|
||||
/** Duration in ms the circuit stays open before allowing a probe (5 minutes) */
|
||||
const CIRCUIT_COOLDOWN_MS = 300_000
|
||||
|
||||
/** Interval between balance/auth probe checks for fatal errors (5 minutes) */
|
||||
const FATAL_PROBE_INTERVAL_MS = 300_000
|
||||
|
||||
export type ErrorKind = "fatal" | "retriable" | "transient"
|
||||
|
||||
/**
|
||||
* Extract an HTTP status code from an error message like "SSE failed: 402 Payment Required"
|
||||
* or "FIM request failed: 429 Too Many Requests".
|
||||
* Only matches 4xx/5xx after a colon — the two error sources (SDK SSE client and
|
||||
* gateway FIM route) both use this format.
|
||||
*/
|
||||
export function extractStatus(error: unknown): number | null {
|
||||
const msg = error instanceof Error ? error.message : String(error)
|
||||
const match = msg.match(/:\s*([45]\d{2})\b/)
|
||||
return match ? Number(match[1]) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify an error into one of three categories based on its HTTP status code.
|
||||
*/
|
||||
export function classify(error: unknown): ErrorKind {
|
||||
const status = extractStatus(error)
|
||||
if (status === null) return "transient"
|
||||
if (status === 401 || status === 402 || status === 403) return "fatal"
|
||||
if (status === 429 || status >= 500) return "retriable"
|
||||
return "transient"
|
||||
}
|
||||
|
||||
export class ErrorBackoff {
|
||||
/** The kind of the most recent fatal error, if any */
|
||||
private fatal: ErrorKind | null = null
|
||||
/** HTTP status of the fatal error (for notification messages) */
|
||||
private fatalStatus: number | null = null
|
||||
/** Timestamp when the fatal error was recorded (for probe interval) */
|
||||
private fatalAt = 0
|
||||
/** Timestamp when the circuit was opened (for retriable circuit breaker) */
|
||||
private opened = 0
|
||||
/** Consecutive retriable failure count */
|
||||
private failures = 0
|
||||
/** Timestamp until which requests should be delayed (retriable backoff) */
|
||||
private blockedUntil = 0
|
||||
|
||||
/**
|
||||
* Record a successful request. Resets all backoff/circuit state.
|
||||
*/
|
||||
success(): void {
|
||||
this.fatal = null
|
||||
this.fatalStatus = null
|
||||
this.fatalAt = 0
|
||||
this.failures = 0
|
||||
this.blockedUntil = 0
|
||||
this.opened = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an error and update backoff state.
|
||||
* Returns the error kind for the caller to act on.
|
||||
*/
|
||||
failure(error: unknown): ErrorKind {
|
||||
const kind = classify(error)
|
||||
|
||||
if (kind === "fatal") {
|
||||
this.fatal = kind
|
||||
this.fatalStatus = extractStatus(error)
|
||||
this.fatalAt = Date.now()
|
||||
return kind
|
||||
}
|
||||
|
||||
if (kind === "retriable") {
|
||||
this.failures++
|
||||
// Exponential backoff: 2s, 4s, 8s, 16s, … capped at MAX_DELAY_MS
|
||||
const delay = Math.min(BASE_DELAY_MS * 2 ** (this.failures - 1), MAX_DELAY_MS)
|
||||
this.blockedUntil = Date.now() + delay
|
||||
|
||||
// Open circuit after threshold consecutive failures
|
||||
if (this.failures >= CIRCUIT_THRESHOLD && this.opened === 0) {
|
||||
this.opened = Date.now()
|
||||
}
|
||||
return kind
|
||||
}
|
||||
|
||||
// transient — no state change
|
||||
return kind
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether autocomplete requests should be blocked right now.
|
||||
*/
|
||||
blocked(): boolean {
|
||||
// Fatal errors block until explicitly reset
|
||||
if (this.fatal) return true
|
||||
|
||||
// Circuit breaker is open
|
||||
if (this.opened > 0) {
|
||||
const elapsed = Date.now() - this.opened
|
||||
if (elapsed < CIRCUIT_COOLDOWN_MS) return true
|
||||
// Cooldown expired — allow one probe, reset circuit
|
||||
this.opened = 0
|
||||
this.failures = 0
|
||||
this.blockedUntil = 0
|
||||
return false
|
||||
}
|
||||
|
||||
// Exponential backoff still active
|
||||
if (this.blockedUntil > 0 && Date.now() < this.blockedUntil) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a fatal (non-retriable) error is active — credits depleted, auth invalid, etc.
|
||||
*/
|
||||
isFatal(): boolean {
|
||||
return this.fatal !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* The HTTP status code of the fatal error, or null.
|
||||
*/
|
||||
getFatalStatus(): number | null {
|
||||
return this.fatalStatus
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether it's time for a lightweight probe (e.g. balance check) to see if
|
||||
* the fatal condition has been resolved. Returns true at most once per
|
||||
* FATAL_PROBE_INTERVAL_MS. The caller should check balance/auth and call
|
||||
* reset() if the condition is cleared.
|
||||
*/
|
||||
shouldProbe(): boolean {
|
||||
if (!this.fatal) return false
|
||||
const elapsed = Date.now() - this.fatalAt
|
||||
if (elapsed < FATAL_PROBE_INTERVAL_MS) return false
|
||||
this.fatalAt = Date.now()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually reset all state (e.g. when user adds credits or re-authenticates).
|
||||
*/
|
||||
reset(): void {
|
||||
this.success()
|
||||
}
|
||||
}
|
||||
-1
@@ -47,7 +47,6 @@ export class FimPromptBuilder {
|
||||
}
|
||||
|
||||
return {
|
||||
strategy: "fim",
|
||||
formattedPrefix,
|
||||
prunedSuffix,
|
||||
autocompleteInput,
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import {
|
||||
AutocompleteInput,
|
||||
AutocompleteContextProvider,
|
||||
HoleFillerAutocompletePrompt,
|
||||
FillInAtCursorSuggestion,
|
||||
ChatCompletionResult,
|
||||
} from "../types"
|
||||
import { parseAutocompleteResponse as _parseAutocompleteResponse } from "./hole-filler-utils"
|
||||
import { getProcessedSnippets } from "./getProcessedSnippets"
|
||||
import { formatSnippets } from "../continuedev/core/autocomplete/templating/formatting"
|
||||
import { AutocompleteModel, ApiStreamChunk } from "../AutocompleteModel"
|
||||
|
||||
export type { HoleFillerAutocompletePrompt, FillInAtCursorSuggestion, ChatCompletionResult }
|
||||
|
||||
/**
|
||||
* Parse the response - only handles responses with <COMPLETION> tags
|
||||
* Returns a FillInAtCursorSuggestion with the extracted text, or an empty string if nothing found
|
||||
*/
|
||||
export function parseAutocompleteResponse(
|
||||
fullResponse: string,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
): FillInAtCursorSuggestion {
|
||||
return _parseAutocompleteResponse(fullResponse, prefix, suffix)
|
||||
}
|
||||
|
||||
export class HoleFiller {
|
||||
constructor(private contextProvider: AutocompleteContextProvider) {}
|
||||
|
||||
async getPrompts(autocompleteInput: AutocompleteInput, languageId: string): Promise<HoleFillerAutocompletePrompt> {
|
||||
return {
|
||||
strategy: "hole_filler",
|
||||
systemPrompt: this.getSystemInstructions(),
|
||||
userPrompt: await this.getUserPrompt(autocompleteInput, languageId),
|
||||
autocompleteInput,
|
||||
}
|
||||
}
|
||||
|
||||
getSystemInstructions(): string {
|
||||
return `You are a HOLE FILLER. You are provided with a file containing holes, formatted as '{{FILL_HERE}}'. Your TASK is to complete with a string to replace this hole with, inside a <COMPLETION/> XML tag, including context-aware indentation, if needed. All completions MUST be truthful, accurate, well-written and correct.
|
||||
|
||||
## CRITICAL RULES
|
||||
- NEVER repeat or duplicate content that appears immediately before {{FILL_HERE}}
|
||||
- If {{FILL_HERE}} is at the end of a comment line, start your completion with a newline and new code
|
||||
- Maintain proper indentation matching the surrounding code
|
||||
|
||||
## Context Format
|
||||
<LANGUAGE>: file language
|
||||
<QUERY>: contains commented reference code (// Path: file.ts) followed by code with {{FILL_HERE}}
|
||||
Comments provide context from related files, recent edits, imports, etc.
|
||||
|
||||
## EXAMPLE QUERY:
|
||||
|
||||
<QUERY>
|
||||
function sum_evens(lim) {
|
||||
var sum = 0;
|
||||
for (var i = 0; i < lim; ++i) {
|
||||
{{FILL_HERE}}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
</QUERY>
|
||||
|
||||
TASK: Fill the {{FILL_HERE}} hole.
|
||||
|
||||
## CORRECT COMPLETION
|
||||
|
||||
<COMPLETION>if (i % 2 === 0) {
|
||||
sum += i;
|
||||
}</COMPLETION>
|
||||
|
||||
## EXAMPLE QUERY:
|
||||
|
||||
<QUERY>
|
||||
def sum_list(lst):
|
||||
total = 0
|
||||
for x in lst:
|
||||
{{FILL_HERE}}
|
||||
return total
|
||||
|
||||
print sum_list([1, 2, 3])
|
||||
</QUERY>
|
||||
|
||||
## CORRECT COMPLETION:
|
||||
|
||||
<COMPLETION> total += x</COMPLETION>
|
||||
|
||||
## EXAMPLE QUERY:
|
||||
|
||||
<QUERY>
|
||||
// data Tree a = Node (Tree a) (Tree a) | Leaf a
|
||||
|
||||
// sum :: Tree Int -> Int
|
||||
// sum (Node lft rgt) = sum lft + sum rgt
|
||||
// sum (Leaf val) = val
|
||||
|
||||
// convert to TypeScript:
|
||||
{{FILL_HERE}}
|
||||
</QUERY>
|
||||
|
||||
## CORRECT COMPLETION:
|
||||
|
||||
<COMPLETION>type Tree<T>
|
||||
= {$:"Node", lft: Tree<T>, rgt: Tree<T>}
|
||||
| {$:"Leaf", val: T};
|
||||
|
||||
function sum(tree: Tree<number>): number {
|
||||
switch (tree.$) {
|
||||
case "Node":
|
||||
return sum(tree.lft) + sum(tree.rgt);
|
||||
case "Leaf":
|
||||
return tree.val;
|
||||
}
|
||||
}</COMPLETION>
|
||||
|
||||
## EXAMPLE QUERY:
|
||||
|
||||
The 5th {{FILL_HERE}} is Jupiter.
|
||||
|
||||
## CORRECT COMPLETION:
|
||||
|
||||
<COMPLETION>planet from the Sun</COMPLETION>
|
||||
|
||||
## EXAMPLE QUERY:
|
||||
|
||||
function hypothenuse(a, b) {
|
||||
return Math.sqrt({{FILL_HERE}}b ** 2);
|
||||
}
|
||||
|
||||
## CORRECT COMPLETION:
|
||||
|
||||
<COMPLETION>a ** 2 + </COMPLETION>
|
||||
|
||||
Task: Auto-Completion
|
||||
Provide a subtle, non-intrusive completion after a typing pause.
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Build minimal prompt for auto-trigger with optional context
|
||||
*/
|
||||
async getUserPrompt(autocompleteInput: AutocompleteInput, languageId: string): Promise<string> {
|
||||
const { helper, snippetsWithUris, workspaceDirs } = await getProcessedSnippets(
|
||||
autocompleteInput,
|
||||
autocompleteInput.filepath,
|
||||
this.contextProvider.contextService,
|
||||
this.contextProvider.model,
|
||||
this.contextProvider.ide,
|
||||
this.contextProvider.ignoreController,
|
||||
)
|
||||
const formattedContext = formatSnippets(helper, snippetsWithUris, workspaceDirs)
|
||||
// Use pruned prefix/suffix from HelperVars (token-limited based on DEFAULT_AUTOCOMPLETE_OPTS)
|
||||
return (
|
||||
`<LANGUAGE>${languageId}</LANGUAGE>\n\n` +
|
||||
`<QUERY>
|
||||
${formattedContext}${formattedContext ? "\n" : ""}${helper.prunedPrefix}{{FILL_HERE}}${helper.prunedSuffix}
|
||||
</QUERY>
|
||||
|
||||
TASK: Fill the {{FILL_HERE}} hole. Answer only with the CORRECT completion, and NOTHING ELSE. Do it now.
|
||||
Return the COMPLETION tags`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute chat-based completion using the model
|
||||
*/
|
||||
async getFromChat(
|
||||
model: AutocompleteModel,
|
||||
prompt: HoleFillerAutocompletePrompt,
|
||||
processSuggestion: (text: string) => FillInAtCursorSuggestion,
|
||||
): Promise<ChatCompletionResult> {
|
||||
const { systemPrompt, userPrompt } = prompt
|
||||
let response = ""
|
||||
let chunkCount = 0
|
||||
|
||||
const onChunk = (chunk: ApiStreamChunk) => {
|
||||
if (chunk.type === "text") {
|
||||
response += chunk.text
|
||||
chunkCount++
|
||||
}
|
||||
}
|
||||
|
||||
const usageInfo = await model.generateResponse(systemPrompt, userPrompt, onChunk)
|
||||
|
||||
// Extract just the text from the response - prefix/suffix are handled by the caller
|
||||
const { text: suggestionText } = _parseAutocompleteResponse(response, "", "")
|
||||
|
||||
const fillInAtCursorSuggestion = processSuggestion(suggestionText)
|
||||
|
||||
return {
|
||||
suggestion: fillInAtCursorSuggestion,
|
||||
cost: usageInfo.cost,
|
||||
inputTokens: usageInfo.inputTokens,
|
||||
outputTokens: usageInfo.outputTokens,
|
||||
cacheWriteTokens: usageInfo.cacheWriteTokens,
|
||||
cacheReadTokens: usageInfo.cacheReadTokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
-75
@@ -842,7 +842,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
// Create mock dependencies
|
||||
mockModel = {
|
||||
generateResponse: vi.fn().mockResolvedValue({
|
||||
generateFimResponse: vi.fn().mockResolvedValue({
|
||||
cost: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
@@ -851,7 +851,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
}),
|
||||
getModelName: vi.fn().mockReturnValue("test-model"),
|
||||
getProviderDisplayName: vi.fn().mockReturnValue("test-provider"),
|
||||
supportsFim: vi.fn().mockReturnValue(false), // Default to false for non-FIM tests
|
||||
supportsFim: vi.fn().mockReturnValue(true),
|
||||
hasValidCredentials: vi.fn().mockReturnValue(true), // Default to true for tests
|
||||
} as unknown as AutocompleteModel
|
||||
mockCostTrackingCallback = vi.fn() as CostTrackingCallback
|
||||
@@ -1421,7 +1421,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
// Should return empty array because auto-trigger is disabled
|
||||
expect(result).toEqual([])
|
||||
// Model should not be called
|
||||
expect(mockModel.generateResponse).not.toHaveBeenCalled()
|
||||
expect(mockModel.generateFimResponse).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should block manual trigger when auto-trigger is disabled (defense in depth)", async () => {
|
||||
@@ -1439,7 +1439,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
// Should return empty array as defense in depth, even for manual triggers
|
||||
// The provider should be deregistered at the manager level when disabled
|
||||
expect(result).toEqual([])
|
||||
expect(mockModel.generateResponse).not.toHaveBeenCalled()
|
||||
expect(mockModel.generateFimResponse).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should read settings dynamically on each call", async () => {
|
||||
@@ -1453,7 +1453,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
// First call with auto-trigger enabled
|
||||
await provideWithDebounce(mockDocument, mockPosition, autoContext, mockToken)
|
||||
expect(mockModel.generateResponse).toHaveBeenCalledTimes(1)
|
||||
expect(mockModel.generateFimResponse).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Change settings to disable auto-trigger
|
||||
mockSettings = { enableAutoTrigger: false }
|
||||
@@ -1462,7 +1462,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
const result = await provideWithDebounce(mockDocument, mockPosition, autoContext, mockToken)
|
||||
|
||||
// Should not call model again because auto-trigger is now disabled
|
||||
expect(mockModel.generateResponse).toHaveBeenCalledTimes(1)
|
||||
expect(mockModel.generateFimResponse).toHaveBeenCalledTimes(1)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
@@ -1479,7 +1479,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
// Should default to false (disabled) when settings are null
|
||||
expect(result).toEqual([])
|
||||
expect(mockModel.generateResponse).not.toHaveBeenCalled()
|
||||
expect(mockModel.generateFimResponse).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should allow auto-trigger when explicitly enabled", async () => {
|
||||
@@ -1494,14 +1494,14 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
await provideWithDebounce(mockDocument, mockPosition, autoContext, mockToken)
|
||||
|
||||
// Model should be called because auto-trigger is enabled
|
||||
expect(mockModel.generateResponse).toHaveBeenCalled()
|
||||
expect(mockModel.generateFimResponse).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("failed lookups cache", () => {
|
||||
it("should cache failed LLM lookups and not call LLM again for same prefix/suffix", async () => {
|
||||
// Mock the model to return empty suggestions
|
||||
vi.mocked(mockModel.generateResponse).mockResolvedValue({
|
||||
vi.mocked(mockModel.generateFimResponse).mockResolvedValue({
|
||||
cost: 0.01,
|
||||
inputTokens: 100,
|
||||
outputTokens: 50,
|
||||
@@ -1518,11 +1518,11 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
)) as vscode.InlineCompletionItem[]
|
||||
|
||||
expect(result1).toHaveLength(0)
|
||||
expect(mockModel.generateResponse).toHaveBeenCalledTimes(1)
|
||||
expect(mockModel.generateFimResponse).toHaveBeenCalledTimes(1)
|
||||
expect(mockCostTrackingCallback).toHaveBeenCalledWith(0.01, 100, 50)
|
||||
|
||||
// Second call with same prefix/suffix - should NOT invoke LLM
|
||||
vi.mocked(mockModel.generateResponse).mockClear()
|
||||
vi.mocked(mockModel.generateFimResponse).mockClear()
|
||||
vi.mocked(mockCostTrackingCallback).mockClear()
|
||||
|
||||
const result2 = (await provideWithDebounce(
|
||||
@@ -1533,20 +1533,18 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
)) as vscode.InlineCompletionItem[]
|
||||
|
||||
expect(result2).toHaveLength(0)
|
||||
expect(mockModel.generateResponse).not.toHaveBeenCalled()
|
||||
expect(mockModel.generateFimResponse).not.toHaveBeenCalled()
|
||||
expect(mockCostTrackingCallback).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not cache successful LLM lookups in failed cache", async () => {
|
||||
// Mock the model to return a successful suggestion using proper COMPLETION format
|
||||
let callCount = 0
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async (_sys, _user, onChunk) => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async (_prefix, _suffix, onChunk) => {
|
||||
callCount++
|
||||
// Simulate streaming response with proper COMPLETION format expected by parser
|
||||
// Simulate streaming FIM response
|
||||
if (onChunk) {
|
||||
onChunk({ type: "text", text: "<COMPLETION>" })
|
||||
onChunk({ type: "text", text: "console.log('success');" })
|
||||
onChunk({ type: "text", text: "</COMPLETION>" })
|
||||
onChunk("console.log('success');")
|
||||
}
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -1584,7 +1582,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
it("should cache different prefix/suffix combinations separately", async () => {
|
||||
// Mock the model to return empty suggestions
|
||||
let callCount = 0
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async () => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async () => {
|
||||
callCount++
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -1618,7 +1616,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
it("should maintain only the last 50 failed lookups (FIFO)", async () => {
|
||||
// Mock the model to return empty suggestions
|
||||
let callCount = 0
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async () => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async () => {
|
||||
callCount++
|
||||
return {
|
||||
cost: 0,
|
||||
@@ -1664,7 +1662,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
it("should not add duplicate failed lookups", async () => {
|
||||
// Mock the model to return empty suggestions
|
||||
vi.mocked(mockModel.generateResponse).mockResolvedValue({
|
||||
vi.mocked(mockModel.generateFimResponse).mockResolvedValue({
|
||||
cost: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
@@ -1674,22 +1672,22 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
// First call - adds to failed cache
|
||||
await provideWithDebounce(mockDocument, mockPosition, mockContext, mockToken)
|
||||
expect(mockModel.generateResponse).toHaveBeenCalledTimes(1)
|
||||
expect(mockModel.generateFimResponse).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Second call - should use cache, not add duplicate
|
||||
vi.mocked(mockModel.generateResponse).mockClear()
|
||||
vi.mocked(mockModel.generateFimResponse).mockClear()
|
||||
await provideWithDebounce(mockDocument, mockPosition, mockContext, mockToken)
|
||||
expect(mockModel.generateResponse).not.toHaveBeenCalled()
|
||||
expect(mockModel.generateFimResponse).not.toHaveBeenCalled()
|
||||
|
||||
// Third call - should still use cache
|
||||
vi.mocked(mockModel.generateResponse).mockClear()
|
||||
vi.mocked(mockModel.generateFimResponse).mockClear()
|
||||
await provideWithDebounce(mockDocument, mockPosition, mockContext, mockToken)
|
||||
expect(mockModel.generateResponse).not.toHaveBeenCalled()
|
||||
expect(mockModel.generateFimResponse).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should return empty result with zero cost when using failed cache", async () => {
|
||||
// Mock the model to return empty suggestions
|
||||
vi.mocked(mockModel.generateResponse).mockResolvedValue({
|
||||
vi.mocked(mockModel.generateFimResponse).mockResolvedValue({
|
||||
cost: 0.01,
|
||||
inputTokens: 100,
|
||||
outputTokens: 50,
|
||||
@@ -1711,11 +1709,9 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
describe("useless suggestion filtering", () => {
|
||||
it("should refuse suggestions that match the end of prefix", async () => {
|
||||
// Mock the model to return a suggestion that matches the end of prefix
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async (_sys, _user, onChunk) => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async (_prefix, _suffix, onChunk) => {
|
||||
if (onChunk) {
|
||||
onChunk({ type: "text", text: "<COMPLETION>" })
|
||||
onChunk({ type: "text", text: "= 1" }) // This matches the end of "const x = 1"
|
||||
onChunk({ type: "text", text: "</COMPLETION>" })
|
||||
onChunk("= 1") // This matches the end of "const x = 1"
|
||||
}
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -1735,16 +1731,14 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
// Should return empty array because the suggestion is useless
|
||||
expect(result).toHaveLength(0)
|
||||
expect(mockModel.generateResponse).toHaveBeenCalledTimes(1)
|
||||
expect(mockModel.generateFimResponse).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should refuse suggestions that match the start of suffix", async () => {
|
||||
// Mock the model to return a suggestion that matches the start of suffix
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async (_sys, _user, onChunk) => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async (_prefix, _suffix, onChunk) => {
|
||||
if (onChunk) {
|
||||
onChunk({ type: "text", text: "<COMPLETION>" })
|
||||
onChunk({ type: "text", text: "\nconst" }) // This matches the start of "\nconst y = 2"
|
||||
onChunk({ type: "text", text: "</COMPLETION>" })
|
||||
onChunk("\nconst") // This matches the start of "\nconst y = 2"
|
||||
}
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -1764,16 +1758,14 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
// Should return empty array because the suggestion is useless
|
||||
expect(result).toHaveLength(0)
|
||||
expect(mockModel.generateResponse).toHaveBeenCalledTimes(1)
|
||||
expect(mockModel.generateFimResponse).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should accept useful suggestions that don't match prefix end or suffix start", async () => {
|
||||
// Mock the model to return a useful suggestion
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async (_sys, _user, onChunk) => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async (_prefix, _suffix, onChunk) => {
|
||||
if (onChunk) {
|
||||
onChunk({ type: "text", text: "<COMPLETION>" })
|
||||
onChunk({ type: "text", text: "\nconsole.log('useful');" }) // Useful suggestion
|
||||
onChunk({ type: "text", text: "</COMPLETION>" })
|
||||
onChunk("\nconsole.log('useful');") // Useful suggestion
|
||||
}
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -1794,16 +1786,14 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
// Should return the suggestion because it's useful
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].insertText).toBe("\nconsole.log('useful');")
|
||||
expect(mockModel.generateResponse).toHaveBeenCalledTimes(1)
|
||||
expect(mockModel.generateFimResponse).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should cache refused suggestions as empty to avoid repeated LLM calls", async () => {
|
||||
// Mock the model to return a useless suggestion
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async (_sys, _user, onChunk) => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async (_prefix, _suffix, onChunk) => {
|
||||
if (onChunk) {
|
||||
onChunk({ type: "text", text: "<COMPLETION>" })
|
||||
onChunk({ type: "text", text: "= 1" }) // Matches end of prefix
|
||||
onChunk({ type: "text", text: "</COMPLETION>" })
|
||||
onChunk("= 1") // Matches end of prefix
|
||||
}
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -1823,10 +1813,10 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
)) as vscode.InlineCompletionItem[]
|
||||
|
||||
expect(result1).toHaveLength(0)
|
||||
expect(mockModel.generateResponse).toHaveBeenCalledTimes(1)
|
||||
expect(mockModel.generateFimResponse).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Second call with same prefix/suffix - should use cache, not call LLM
|
||||
vi.mocked(mockModel.generateResponse).mockClear()
|
||||
vi.mocked(mockModel.generateFimResponse).mockClear()
|
||||
const result2 = (await provideWithDebounce(
|
||||
mockDocument,
|
||||
mockPosition,
|
||||
@@ -1835,7 +1825,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
)) as vscode.InlineCompletionItem[]
|
||||
|
||||
expect(result2).toHaveLength(0)
|
||||
expect(mockModel.generateResponse).not.toHaveBeenCalled()
|
||||
expect(mockModel.generateFimResponse).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1861,13 +1851,13 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
// Should return empty array because credentials are not valid
|
||||
expect(result).toHaveLength(0)
|
||||
// Model should not be called
|
||||
expect(mockModel.generateResponse).not.toHaveBeenCalled()
|
||||
expect(mockModel.generateFimResponse).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not attempt an LLM call if credentials become invalid before the debounced fetch executes", async () => {
|
||||
// First call executes immediately (leading edge) - just to move provider into debounced mode
|
||||
await provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken)
|
||||
expect(mockModel.generateResponse).toHaveBeenCalledTimes(1)
|
||||
expect(mockModel.generateFimResponse).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Second call will be debounced. Simulate a model reload happening before the timer fires.
|
||||
vi.mocked(mockModel.hasValidCredentials).mockReturnValue(false)
|
||||
@@ -1878,8 +1868,8 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
await promise
|
||||
|
||||
// If fetch-time validation is working, we do not call generateResponse again.
|
||||
expect(mockModel.generateResponse).toHaveBeenCalledTimes(1)
|
||||
// If fetch-time validation is working, we do not call generateFimResponse again.
|
||||
expect(mockModel.generateFimResponse).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should return suggestions when model has valid credentials", async () => {
|
||||
@@ -1940,12 +1930,10 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
it("should reuse pending request when user types forward (prefix extends, suffix unchanged)", async () => {
|
||||
// Mock the model to track call count
|
||||
let callCount = 0
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async (_sys, _user, onChunk) => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async (_prefix, _suffix, onChunk) => {
|
||||
callCount++
|
||||
if (onChunk) {
|
||||
onChunk({ type: "text", text: "<COMPLETION>" })
|
||||
onChunk({ type: "text", text: "console.log('test');" })
|
||||
onChunk({ type: "text", text: "</COMPLETION>" })
|
||||
onChunk("console.log('test');")
|
||||
}
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -1988,12 +1976,10 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
it("should NOT reuse pending request when suffix changes", async () => {
|
||||
// Mock the model to track call count
|
||||
let callCount = 0
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async (_sys, _user, onChunk) => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async (_prefix, _suffix, onChunk) => {
|
||||
callCount++
|
||||
if (onChunk) {
|
||||
onChunk({ type: "text", text: "<COMPLETION>" })
|
||||
onChunk({ type: "text", text: "console.log('test');" })
|
||||
onChunk({ type: "text", text: "</COMPLETION>" })
|
||||
onChunk("console.log('test');")
|
||||
}
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -2038,12 +2024,10 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
it("should NOT reuse pending request when user backspaces (prefix shrinks)", async () => {
|
||||
// Mock the model to track call count
|
||||
let callCount = 0
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async (_sys, _user, onChunk) => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async (_prefix, _suffix, onChunk) => {
|
||||
callCount++
|
||||
if (onChunk) {
|
||||
onChunk({ type: "text", text: "<COMPLETION>" })
|
||||
onChunk({ type: "text", text: "console.log('test');" })
|
||||
onChunk({ type: "text", text: "</COMPLETION>" })
|
||||
onChunk("console.log('test');")
|
||||
}
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -2087,7 +2071,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
describe("debounce with leading edge behavior", () => {
|
||||
it("should execute immediately on first call (leading edge)", async () => {
|
||||
vi.mocked(mockModel.generateResponse).mockResolvedValue({
|
||||
vi.mocked(mockModel.generateFimResponse).mockResolvedValue({
|
||||
cost: 0.01,
|
||||
inputTokens: 100,
|
||||
outputTokens: 50,
|
||||
@@ -2100,12 +2084,12 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
// Model should be called immediately (no timer needed)
|
||||
await promise
|
||||
expect(mockModel.generateResponse).toHaveBeenCalledTimes(1)
|
||||
expect(mockModel.generateFimResponse).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should debounce subsequent calls (wait for 300ms of inactivity)", async () => {
|
||||
let callCount = 0
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async () => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async () => {
|
||||
callCount++
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -2138,7 +2122,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
it("should reset debounce timer on each call (only execute after 300ms of inactivity)", async () => {
|
||||
let callCount = 0
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async () => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async () => {
|
||||
callCount++
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -2184,7 +2168,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
it("should allow immediate execution after debounce completes (new leading edge)", async () => {
|
||||
let callCount = 0
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async () => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async () => {
|
||||
callCount++
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -2222,7 +2206,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
describe("adaptive debounce delay", () => {
|
||||
it("should start with initial debounce delay of 300ms", async () => {
|
||||
let callCount = 0
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async () => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async () => {
|
||||
callCount++
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -2321,7 +2305,7 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
it("should use adaptive debounce delay after collecting enough samples", async () => {
|
||||
let callCount = 0
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async () => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async () => {
|
||||
callCount++
|
||||
return {
|
||||
cost: 0.01,
|
||||
@@ -2365,12 +2349,10 @@ describe("AutocompleteInlineCompletionProvider", () => {
|
||||
|
||||
it("should record latency from LLM requests", async () => {
|
||||
// Mock the model to simulate a delay
|
||||
vi.mocked(mockModel.generateResponse).mockImplementation(async (_sys, _user, onChunk) => {
|
||||
vi.mocked(mockModel.generateFimResponse).mockImplementation(async (_prefix, _suffix, onChunk) => {
|
||||
// Simulate some processing time
|
||||
if (onChunk) {
|
||||
onChunk({ type: "text", text: "<COMPLETION>" })
|
||||
onChunk({ type: "text", text: "console.log('test');" })
|
||||
onChunk({ type: "text", text: "</COMPLETION>" })
|
||||
onChunk("console.log('test');")
|
||||
}
|
||||
return {
|
||||
cost: 0.01,
|
||||
|
||||
-301
@@ -1,301 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest"
|
||||
import { HoleFiller, parseAutocompleteResponse } from "../HoleFiller"
|
||||
import { AutocompleteInput, AutocompleteContextProvider } from "../../types"
|
||||
import crypto from "crypto"
|
||||
import { AutocompleteSnippetType } from "../../continuedev/core/autocomplete/types"
|
||||
|
||||
// Mock the getProcessedSnippets module
|
||||
vi.mock("../getProcessedSnippets", () => ({
|
||||
getProcessedSnippets: vi.fn().mockResolvedValue({
|
||||
filepathUri: "file:///test.ts",
|
||||
helper: {
|
||||
filepath: "file:///test.ts",
|
||||
lang: { name: "typescript", singleLineComment: "//" },
|
||||
prunedPrefix: "const x = 1;\n",
|
||||
prunedSuffix: "",
|
||||
},
|
||||
snippetsWithUris: [],
|
||||
workspaceDirs: [],
|
||||
}),
|
||||
}))
|
||||
|
||||
function createAutocompleteInput(
|
||||
filepath: string = "/test.ts",
|
||||
line: number = 0,
|
||||
character: number = 0,
|
||||
): AutocompleteInput {
|
||||
return {
|
||||
isUntitledFile: false,
|
||||
completionId: crypto.randomUUID(),
|
||||
filepath,
|
||||
pos: { line, character },
|
||||
recentlyVisitedRanges: [],
|
||||
recentlyEditedRanges: [],
|
||||
}
|
||||
}
|
||||
|
||||
describe("HoleFiller", () => {
|
||||
let holeFiller: HoleFiller
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Create a minimal mock context provider for basic tests
|
||||
const mockContextProvider: AutocompleteContextProvider = {
|
||||
contextService: {
|
||||
initializeForFile: vi.fn().mockResolvedValue(undefined),
|
||||
} as any,
|
||||
model: {
|
||||
getModelName: () => "codestral",
|
||||
} as any,
|
||||
ide: {
|
||||
getWorkspaceDirs: vi.fn().mockResolvedValue([]),
|
||||
} as any,
|
||||
ignoreController: undefined,
|
||||
}
|
||||
|
||||
holeFiller = new HoleFiller(mockContextProvider)
|
||||
})
|
||||
|
||||
describe("getPrompts", () => {
|
||||
it("should generate prompts with QUERY/FILL_HERE format", async () => {
|
||||
const { systemPrompt, userPrompt } = await holeFiller.getPrompts(
|
||||
createAutocompleteInput("/test.ts", 0, 13),
|
||||
"typescript",
|
||||
)
|
||||
|
||||
// Verify system prompt contains auto-trigger keywords
|
||||
expect(systemPrompt).toContain("Auto-Completion")
|
||||
expect(systemPrompt).toContain("Context Format")
|
||||
|
||||
const expected = `<LANGUAGE>typescript</LANGUAGE>
|
||||
|
||||
<QUERY>
|
||||
|
||||
// test.ts
|
||||
const x = 1;
|
||||
{{FILL_HERE}}
|
||||
</QUERY>
|
||||
|
||||
TASK: Fill the {{FILL_HERE}} hole. Answer only with the CORRECT completion, and NOTHING ELSE. Do it now.
|
||||
Return the COMPLETION tags`
|
||||
|
||||
expect(userPrompt).toBe(expected)
|
||||
})
|
||||
|
||||
it("should include comment-wrapped context when provider is set", async () => {
|
||||
// Update the mock for this specific test
|
||||
const { getProcessedSnippets } = await import("../getProcessedSnippets")
|
||||
;(getProcessedSnippets as any).mockResolvedValueOnce({
|
||||
filepathUri: "file:///app.ts",
|
||||
helper: {
|
||||
filepath: "file:///app.ts",
|
||||
lang: { name: "typescript", singleLineComment: "//" },
|
||||
prunedPrefix: "function calculate() {\n ",
|
||||
prunedSuffix: "\n}",
|
||||
},
|
||||
snippetsWithUris: [
|
||||
{
|
||||
filepath: "file:///utils.ts",
|
||||
content: "export function sum(a: number, b: number) {\n return a + b\n}",
|
||||
type: AutocompleteSnippetType.Code,
|
||||
},
|
||||
],
|
||||
workspaceDirs: ["file:///workspace"],
|
||||
})
|
||||
|
||||
const mockContextProvider: AutocompleteContextProvider = {
|
||||
contextService: {
|
||||
initializeForFile: vi.fn().mockResolvedValue(undefined),
|
||||
} as any,
|
||||
model: {
|
||||
getModelName: () => "codestral",
|
||||
} as any,
|
||||
ide: {
|
||||
getWorkspaceDirs: vi.fn().mockResolvedValue(["file:///workspace"]),
|
||||
} as any,
|
||||
ignoreController: undefined,
|
||||
}
|
||||
|
||||
const holeFillerWithContext = new HoleFiller(mockContextProvider)
|
||||
const { userPrompt } = await holeFillerWithContext.getPrompts(
|
||||
createAutocompleteInput("/app.ts", 5, 0),
|
||||
"typescript",
|
||||
)
|
||||
|
||||
// Use toContain for key parts instead of exact match to avoid whitespace issues
|
||||
expect(userPrompt).toContain("<LANGUAGE>typescript</LANGUAGE>")
|
||||
expect(userPrompt).toContain("// Path: utils.ts")
|
||||
expect(userPrompt).toContain("// export function sum(a: number, b: number)")
|
||||
expect(userPrompt).toContain("function calculate() {")
|
||||
expect(userPrompt).toContain("{{FILL_HERE}}")
|
||||
expect(userPrompt).toContain("TASK: Fill the {{FILL_HERE}} hole")
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseAutocompleteResponse", () => {
|
||||
const prefix = "function test() {\n "
|
||||
const suffix = "\n}"
|
||||
|
||||
describe("Response parsing with COMPLETION tags", () => {
|
||||
it("should extract content between COMPLETION tags", () => {
|
||||
const response = "<COMPLETION>return 42</COMPLETION>"
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe("return 42")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
|
||||
it("should handle multiline content in COMPLETION tags", () => {
|
||||
const response = "<COMPLETION>const x = 1;\nconst y = 2;\nreturn x + y;</COMPLETION>"
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe("const x = 1;\nconst y = 2;\nreturn x + y;")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
|
||||
it("should handle incomplete COMPLETION tag (streaming)", () => {
|
||||
const response = "<COMPLETION>return 42"
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
// Incomplete tags should return empty string
|
||||
expect(result.text).toBe("")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
|
||||
it("should remove any accidental tag remnants", () => {
|
||||
const response = "<COMPLETION>return 42<COMPLETION></COMPLETION>"
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe("return 42")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
|
||||
it("should handle case-insensitive tags", () => {
|
||||
const response = "<completion>return 42</completion>"
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe("return 42")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Response parsing without COMPLETION tags (no suggestions)", () => {
|
||||
it("should return empty string when no tags present", () => {
|
||||
const response = "return 42"
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe("")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
|
||||
it("should return empty string for multiline response without tags", () => {
|
||||
const response = "const x = 1;\nconst y = 2;\nreturn x + y;"
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe("")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
|
||||
it("should return empty string for markdown code blocks without tags", () => {
|
||||
const response = "```typescript\nreturn 42\n```"
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe("")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("should handle empty response", () => {
|
||||
const response = ""
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe("")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
|
||||
it("should return empty string for whitespace-only response without tags", () => {
|
||||
const response = " \n\t "
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe("")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
|
||||
it("should handle custom prefix/suffix with COMPLETION tags", () => {
|
||||
const customPrefix = "const greeting = "
|
||||
const customSuffix = ";"
|
||||
const response = '<COMPLETION>"Hello, World!"</COMPLETION>'
|
||||
|
||||
const result = parseAutocompleteResponse(response, customPrefix, customSuffix)
|
||||
|
||||
expect(result.text).toBe('"Hello, World!"')
|
||||
expect(result.prefix).toBe(customPrefix)
|
||||
expect(result.suffix).toBe(customSuffix)
|
||||
})
|
||||
|
||||
it("should handle empty COMPLETION tags", () => {
|
||||
const response = "<COMPLETION></COMPLETION>"
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe("")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
|
||||
it("should handle whitespace-only content in COMPLETION tags", () => {
|
||||
const response = "<COMPLETION> </COMPLETION>"
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe(" ")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
|
||||
it("should handle response with extra text before COMPLETION tag", () => {
|
||||
const response = "Here is the code:\n<COMPLETION>return 42</COMPLETION>"
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe("return 42")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
|
||||
it("should handle response with extra text after COMPLETION tag", () => {
|
||||
const response = "<COMPLETION>return 42</COMPLETION>\nThat's the code!"
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
|
||||
expect(result.text).toBe("return 42")
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
})
|
||||
|
||||
describe("performance", () => {
|
||||
it("should handle large responses efficiently", () => {
|
||||
const largeContent = "x".repeat(10000)
|
||||
const response = `<COMPLETION>${largeContent}</COMPLETION>`
|
||||
|
||||
const startTime = performance.now()
|
||||
const result = parseAutocompleteResponse(response, prefix, suffix)
|
||||
const endTime = performance.now()
|
||||
|
||||
expect(endTime - startTime).toBeLessThan(100)
|
||||
expect(result.text).toBe(largeContent)
|
||||
expect(result.prefix).toBe(prefix)
|
||||
expect(result.suffix).toBe(suffix)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import type { FillInAtCursorSuggestion } from "../types"
|
||||
|
||||
/**
|
||||
* Parse a chat completion response and extract the text between <COMPLETION> tags.
|
||||
* Returns a FillInAtCursorSuggestion with the extracted text, or empty string if not found.
|
||||
*/
|
||||
export function parseAutocompleteResponse(
|
||||
fullResponse: string,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
): FillInAtCursorSuggestion {
|
||||
let fimText = ""
|
||||
const completionMatch = fullResponse.match(/<COMPLETION>([\s\S]*?)<\/COMPLETION>/i)
|
||||
if (completionMatch) {
|
||||
fimText = completionMatch[1] || ""
|
||||
}
|
||||
fimText = fimText.replace(/<\/?COMPLETION>/gi, "")
|
||||
return { text: fimText, prefix, suffix }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user