Merge branch 'dev' into feat/suggestions-after-plan-end

This commit is contained in:
Marian Alexandru Alecu
2026-02-20 08:39:44 +02:00
committed by GitHub
67 changed files with 3212 additions and 260 deletions
+55 -57
View File
@@ -1,18 +1,53 @@
<p align="center">
<a href="https://kilo.ai">
<img src="logo.png" alt="Kilo CLI logo">
</a>
</p>
<p align="center">The open source AI coding agent.</p>
<p align="center">
<a href="https://kilo.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://x.com/kilocode"><img alt="X" src="https://img.shields.io/badge/X-@kilocode-000000?style=flat-square&logo=x&logoColor=white" /></a>
<a href="https://www.reddit.com/r/kilocode/"><img alt="Reddit" src="https://img.shields.io/badge/Reddit-r%2Fkilocode-FF4500?style=flat-square&logo=reddit&logoColor=white" /></a>
<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>
</p>
---
# 🚀 Kilo
### Installation
> Kilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.
> #1 on OpenRouter. 1.5M+ Kilo Coders. 25T+ tokens processed
- ✨ Generate code from natural language
- ✅ Checks its own work
- 🧪 Run terminal commands
- 🌐 Automate the browser
- ⚡ Inline autocomplete suggestions
- 🤖 Latest AI models
- 🎁 API keys optional
- 💡 **Get $20 in bonus credits when you top-up for the first time** Credits can be used with 500+ models like Gemini 3 Pro, Claude 4.5 Sonnet & Opus, and GPT-5
<p align="center">
<img src="https://media.githubusercontent.com/media/Kilo-Org/kilocode/main/kilo.gif" width="100%" />
</p>
## Quick Links
- [VS Code Marketplace](https://kilo.ai/vscode-marketplace?utm_source=Readme) (download)
- Install CLI: `npm install -g @kilocode/cli`
- [Official Kilo.ai Home page](https://kilo.ai) (learn more)
## Key Features
- **Code Generation:** Kilo can generate code using natural language.
- **Inline Autocomplete:** Get intelligent code completions as you type, powered by AI.
- **Task Automation:** Kilo can automate repetitive coding tasks to save time.
- **Automated Refactoring:** Kilo can refactor and improve existing code efficiently.
- **MCP Server Marketplace**: Kilo can easily find, and use MCP servers to extend the agent capabilities.
- **Multi Mode**: Plan with Architect, Code with Coder, and Debug with Debugger, and make your own custom modes.
## Get Started in Visual Studio Code
1. Install the Kilo Code extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code).
2. Create your account to access 500+ cutting-edge AI models including Gemini 3 Pro, Claude 4.5 Sonnet & Opus, and GPT-5 with transparent pricing that matches provider rates exactly.
3. Start coding with AI that adapts to your workflow. Watch our quick-start guide to see Kilo in action:
[![Watch the video](https://img.youtube.com/vi/pqGfYXgrhig/maxresdefault.jpg)](https://youtu.be/pqGfYXgrhig)
## Get Started with the CLI
```bash
# npm
@@ -24,19 +59,6 @@ npx @kilocode/cli
Then run `kilo` in any project directory to start.
### Agents
Kilo CLI includes two built-in agents you can switch between using the `Tab` key:
- **build** - Default, full access agent for development work
- **plan** - Read-only agent for analysis and code exploration
- Denies file edits by default
- Asks permission before running bash commands
- Ideal for exploring unfamiliar codebases or planning changes
Also included is a **general** subagent for complex searches and multi-step tasks.
This is used internally and can be invoked using `@general` in messages.
### Autonomous Mode (CI/CD)
Use the `--auto` flag with `kilo run` to enable fully autonomous operation without user interaction. This is ideal for CI/CD pipelines and automated workflows:
@@ -47,44 +69,20 @@ kilo run --auto "run tests and fix any failures"
**Important:** The `--auto` flag disables all permission prompts and allows the agent to execute any action without confirmation. Only use this in trusted environments like CI/CD pipelines.
### Migrating from Kilo Code Extension
## Contributing
If you're coming from the Kilo Code VS Code extension, your configurations are automatically migrated:
We welcome contributions from developers, writers, and enthusiasts!
To get started, please read our [Contributing Guide](/CONTRIBUTING.md). It includes details on setting up your environment, coding standards, types of contribution and how to submit pull requests.
| Kilo Code Feature | Kilo CLI Equivalent |
| -------------------------------------------- | -------------------------------------------- |
| Custom modes | Converted to agents |
| Rules (`.kilocoderules`, `.kilocode/rules/`) | Added to `instructions` array |
| Skills (`.kilocode/skills/`) | Auto-discovered alongside `.opencode/skill/` |
| Workflows (`.kilocode/workflows/`) | Converted to commands |
| MCP servers | Migrated to `mcp` config |
## Code of Conduct
MCP servers are configured in **`~/.config/kilo/opencode.json`** (or `opencode.jsonc`; on Windows the config directory may be under `%USERPROFILE%` depending on your environment). Use a top-level `"mcp"` object: each key is a server name, value is `type: "local"` and `command: ["executable", "arg1", ...]`. Optional per-server: `environment`, `enabled`, `timeout`. Restart the CLI after editing for changes to take effect. (Path from [`packages/opencode/src/global/index.ts`](packages/opencode/src/global/index.ts); schema in [`config.ts`](packages/opencode/src/config/config.ts) `McpLocal`.)
Our community is built on respect, inclusivity, and collaboration. Please review our [Code of Conduct](/CODE_OF_CONDUCT.md) to understand the expectations for all contributors and community members.
**Default mode mappings:**
## License
- `code``build` agent
- `architect``plan` agent
This project is licensed under the MIT License.
Youre free to use, modify, and distribute this code, including for commercial purposes as long as you include proper attribution and license notices. See [License](/LICENSE).
For detailed migration information, see:
- [Migration Overview](packages/opencode/src/kilocode/docs/migration.md)
- [Rules Migration](packages/opencode/src/kilocode/docs/rules-migration.md)
### Documentation
For more info on how to configure Kilo CLI, [**head over to our docs**](https://kilo.ai/docs).
### Contributing
If you're interested in contributing, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request.
### FAQ
#### Where did Kilo CLI come from?
### Where did Kilo CLI come from?
Kilo CLI is a fork of [OpenCode](https://github.com/anomalyco/opencode), enhanced to work within the Kilo agentic engineering platform.
---
**Join our community** [Discord](https://kilo.ai/discord) | [X.com](https://x.com/kilocode) | [Reddit](https://reddit.com/r/kilocode)
+29 -14
View File
@@ -23,7 +23,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.0.23",
"version": "1.0.24",
"dependencies": {
"@kilocode/kilo-i18n": "workspace:*",
"@kilocode/kilo-ui": "workspace:*",
@@ -75,7 +75,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.23",
"version": "1.0.24",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -108,7 +108,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
"version": "1.0.0",
"version": "1.0.24",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -136,7 +136,7 @@
},
"packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway",
"version": "1.0.23",
"version": "1.0.24",
"dependencies": {
"@clack/prompts": "1.0.0-alpha.1",
"@kilocode/plugin": "workspace:*",
@@ -168,7 +168,7 @@
},
"packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n",
"version": "1.0.23",
"version": "1.0.24",
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
@@ -178,7 +178,7 @@
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
"version": "1.0.23",
"version": "1.0.24",
"dependencies": {
"@kilocode/kilo-gateway": "workspace:*",
"@opentelemetry/api": "1.9.0",
@@ -198,7 +198,7 @@
},
"packages/kilo-ui": {
"name": "@kilocode/kilo-ui",
"version": "1.0.23",
"version": "1.0.24",
"dependencies": {
"@kobalte/core": "0.13.11",
},
@@ -221,7 +221,7 @@
},
"packages/kilo-vscode": {
"name": "kilo-code",
"version": "7.0.23",
"version": "7.0.24",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-i18n": "workspace:*",
@@ -257,13 +257,14 @@
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1",
"qrcode": "^1.5.4",
"ts-morph": "27.0.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
},
},
"packages/opencode": {
"name": "@kilocode/cli",
"version": "1.0.23",
"version": "1.0.24",
"bin": {
"kilo": "./bin/kilo",
"kilocode": "./bin/kilocode",
@@ -373,7 +374,7 @@
},
"packages/plugin": {
"name": "@kilocode/plugin",
"version": "1.0.23",
"version": "1.0.24",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"zod": "catalog:",
@@ -387,14 +388,14 @@
},
"packages/script": {
"name": "@opencode-ai/script",
"version": "1.0.23",
"version": "1.0.24",
"devDependencies": {
"@types/bun": "catalog:",
},
},
"packages/sdk/js": {
"name": "@kilocode/sdk",
"version": "1.0.23",
"version": "1.0.24",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -405,7 +406,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.23",
"version": "1.0.24",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
@@ -447,7 +448,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.23",
"version": "1.0.24",
"dependencies": {
"zod": "catalog:",
},
@@ -1582,6 +1583,8 @@
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@ts-morph/common": ["@ts-morph/common@0.28.1", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g=="],
"@tsconfig/bun": ["@tsconfig/bun@1.0.9", "", {}, "sha512-4M0/Ivfwcpz325z6CwSifOBZYji3DFOEpY6zEUt0+Xi2qRhzwvmqQN9XAHJh3OVvRJuAqVTLU2abdCplvp6mwQ=="],
"@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="],
@@ -1914,6 +1917,8 @@
"cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
"code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@@ -2634,6 +2639,8 @@
"partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -3018,6 +3025,8 @@
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
"ts-morph": ["ts-morph@27.0.2", "", { "dependencies": { "@ts-morph/common": "~0.28.1", "code-block-writer": "^13.0.3" } }, "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
@@ -3418,6 +3427,8 @@
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
"@ts-morph/common/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@vscode/test-cli/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
@@ -3772,6 +3783,8 @@
"@tailwindcss/vite/@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="],
"@vscode/test-cli/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@vscode/test-cli/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
@@ -4078,6 +4091,8 @@
"@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="],
"@vscode/test-cli/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"@vscode/test-cli/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-BEhWiPWkXCUblpkXVAIDmWndNbuulgfaDRO80dvssPY=",
"aarch64-linux": "sha256-8ttezrn/CuIJDoAS1SZvq0laoQR6kBcrvv615uCxCJg=",
"aarch64-darwin": "sha256-krlzO1o/VVOC15hneG9W1dgp3Od96QFqZ0wKnjt89c4=",
"x86_64-darwin": "sha256-WjIaEu3jIQTqdWLQ29nacIraxsxZp5YMvzYC0j+pBMY="
"x86_64-linux": "sha256-LVuT8+lBSeogRYQQHruUJJBeSRw9jd3arXy1GLzg9JM=",
"aarch64-linux": "sha256-QDTbTB4Lcc9u5MdEdZZ07GBBbLQkcl2NrwvxRc5WMsk=",
"aarch64-darwin": "sha256-skuEhDRrT585MJL58SpPOqnz0ivrlzShCRkA2f/YIxU=",
"x86_64-darwin": "sha256-Ocf8Mq9dgniaUlUAs0D/ygggh9ozVr8FydkgDsCsEPI="
}
}
+1 -1
View File
@@ -102,6 +102,6 @@
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
},
"version": "1.0.23",
"version": "1.0.24",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.0.23",
"version": "1.0.24",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.0.23",
"version": "1.0.24",
"type": "module",
"license": "MIT",
"scripts": {
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "1.0.23"
version = "1.0.24"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilo"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.23/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.24/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.23/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.24/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.23/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.24/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.23/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.24/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.23/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.24/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "1.0.0",
"version": "1.0.24",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
@@ -49,8 +49,8 @@ Before enabling Code Reviews:
Once configured, the Review Agent runs automatically on PR/MR events. For platform-specific setup, see:
- [GitHub Code Reviews](./github.md)
- [GitLab Code Reviews](./gitlab.md)
- [GitHub Code Reviews](./github)
- [GitLab Code Reviews](./gitlab)
## Local Code Reviews
@@ -220,7 +220,7 @@ For most permissions, you can use an object to apply different actions based on
```json
{
"$schema": "https://opencode.ai/config.json",
"$schema": "https://app.kilo.ai/config.json",
"permission": {
"bash": {
"*": "ask",
@@ -305,7 +305,7 @@ Project-level configuration takes precedence over global settings.
```json
{
"$schema": "https://opencode.ai/config.json",
"$schema": "https://app.kilo.ai/config.json",
"model": "anthropic/claude-sonnet-4-20250514",
"provider": {
"anthropic": {
+1 -1
View File
@@ -30,7 +30,7 @@ const kilo = createOpenAI({
})
const result = streamText({
model: kilo("anthropic/claude-sonnet-4.5"),
model: kilo.chat("anthropic/claude-sonnet-4.5"),
prompt: "Why is the sky blue?",
})
```
@@ -13,7 +13,7 @@ Models are identified using the format `provider/model-name`. Pass this as the `
```typescript
const result = streamText({
model: kilo("anthropic/claude-sonnet-4.5"),
model: kilo.chat("anthropic/claude-sonnet-4.5"),
prompt: "Hello!",
})
```
@@ -53,7 +53,7 @@ const kilo = createOpenAI({
async function main() {
const result = streamText({
model: kilo("anthropic/claude-sonnet-4.5"),
model: kilo.chat("anthropic/claude-sonnet-4.5"),
prompt: "Invent a new holiday and describe its traditions.",
})
@@ -29,7 +29,7 @@ const kilo = createOpenAI({
})
const result = streamText({
model: kilo("anthropic/claude-sonnet-4.5"),
model: kilo.chat("anthropic/claude-sonnet-4.5"),
prompt: "Write a haiku about programming.",
})
@@ -51,7 +51,7 @@ const kilo = createOpenAI({
})
const result = streamText({
model: kilo("anthropic/claude-sonnet-4.5"),
model: kilo.chat("anthropic/claude-sonnet-4.5"),
prompt: "What is the weather in San Francisco?",
tools: {
getWeather: tool({
@@ -86,7 +86,7 @@ export async function POST(request: Request) {
const { messages } = await request.json()
const result = streamText({
model: kilo("anthropic/claude-sonnet-4.5"),
model: kilo.chat("anthropic/claude-sonnet-4.5"),
messages,
})
@@ -37,7 +37,7 @@ const kilo = createOpenAI({
})
const result = streamText({
model: kilo("anthropic/claude-sonnet-4.5"),
model: kilo.chat("anthropic/claude-sonnet-4.5"),
prompt: "Write a short story about a robot.",
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "1.0.23",
"version": "1.0.24",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "1.0.23",
"version": "1.0.24",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
"version": "1.0.23",
"version": "1.0.24",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
"version": "1.0.23",
"version": "1.0.24",
"type": "module",
"license": "MIT",
"exports": {
+2 -1
View File
@@ -1,2 +1,3 @@
bin/
out/
out/
.vscode-test/
+1
View File
@@ -85,6 +85,7 @@ New webview features must use **`@kilocode/kilo-ui`** components instead of raw
- New CSS for components not yet in kilo-ui goes into `chat.css` grouped by comment-delimited sections (`/* Component Name */`). Once a kilo-ui equivalent exists, remove the section.
- **Check the desktop app first**: [`packages/app/src/`](../../packages/app/src/) is the reference implementation for how kilo-ui components are composed together. Always check how the app uses a component before implementing it in the webview — don't just look at the component API in isolation.
- **`data-component` and `data-slot` attributes carry CSS styling** — kilo-ui uses `[data-component]` and `[data-slot]` attribute selectors, not class names. When the app uses e.g. `data-component="permission-prompt"` and `data-slot="permission-actions"`, these get kilo-ui styling for free.
- **Prefer kilo-ui styles**: Always reuse existing kilo-ui CSS variables, tokens, and component styles instead of writing custom CSS. If a style doesn't exist in kilo-ui yet, add it there and reuse it rather than inlining or duplicating styles in the webview.
- **Icons**: kilo-ui has 75+ custom SVG icons in [`packages/ui/src/components/icon.tsx`](../../packages/ui/src/components/icon.tsx). To list all available icon names: `node -e "const c=require('fs').readFileSync('../../packages/ui/src/components/icon.tsx','utf8');[...c.matchAll(/^\\s{2}[\"']?([\\w-]+)[\"']?:\\s*\x60/gm)].map(m=>m[1]).sort().forEach(n=>console.log(n))"`. Icon names use both hyphenated (`arrow-left`) and bare-word (`brain`, `console`, `providers`) keys.
## Debugging
@@ -1,20 +1,29 @@
# Context Menus & Tooltips
# Context Menus & Tooltips (Webview)
Right-click/context actions and tooltip affordances throughout the chat UI.
Right-click/context actions and tooltip affordances within the chat webview UI.
> **Note:** For VS Code-native context menus (editor right-click, terminal right-click, code action lightbulb), see [Editor Context Menus & Code Actions](../non-agent-features/editor-context-menus-and-code-actions.md).
## Scope
This document covers **webview-internal** context menus and tooltips — i.e., right-click menus and hover tooltips rendered inside the Kilo Code chat panel.
## Current State
- kilo-ui provides `Tooltip` and `Popover` components, already in use throughout the webview (TaskHeader, ModelSelector, etc.)
- No webview-internal right-click context menu exists yet
## Remaining Work
- Add hover tooltips with explanatory text for all interactive buttons in the chat UI
- Implement right-click context menus on chat messages (copy, retry, edit, delete)
- Consider context menus on code blocks (copy code, insert at cursor, apply diff)
## Location
- [`webview-ui/src/components/common/ContextMenu.tsx`](../../webview-ui/src/components/common/ContextMenu.tsx:1)
- Various `StandardTooltip` usage
- kilo-ui `Tooltip` component usage throughout webview
## Interactions
## Implementation Notes
- Hover tooltips with explanatory text for all interactive buttons
- Context actions via right-click or dedicated menu buttons
## Suggested migration
**Reimplement?** No (UI-only).
- These are presentation-layer affordances; backend migration does not affect them.
- Kilo CLI has similar tooltip infrastructure in its UI package ([`packages/ui/src/components/tooltip.tsx`](https://github.com/Kilo-Org/kilo/blob/main/packages/ui/src/components/tooltip.tsx:1)), but Kilo can keep its current tooltip + context menu components.
These are presentation-layer affordances; the CLI backend is not involved. kilo-ui already provides the tooltip infrastructure needed.
@@ -1,24 +0,0 @@
# Code Actions
- **What it is**: Integrates into VS Code Code Actions UI (lightbulb/context menu) to trigger AI actions like explain/fix/improve, or add selection to context.
## Notable characteristics
- Can run inside current task or spawn a new task.
- Prompt templates configurable.
## Docs references
- [`apps/kilocode-docs/pages/code-with-ai/features/code-actions.md`](../../apps/kilocode-docs/pages/code-with-ai/features/code-actions.md)
## Suggested migration
- **Kilo CLI availability**: Not present.
- **Migration recommendation**:
- Keep code actions in the VS Code extension host (VS Code APIs, diagnostics, and editor-specific UX).
- Reimplement any backing logic that currently depends on the core agent loop, but keep action registration and application IDE-side.
- **Reimplementation required?**: Yes.
## Primary implementation anchors (partial)
- [`src/services/ghost/GhostCodeActionProvider.ts`](../../src/services/ghost/GhostCodeActionProvider.ts)
@@ -0,0 +1,461 @@
# Editor Context Menus & Code Actions
Full spec of the VS Code-native menu, command, code action, keyboard shortcut, and prompt template system from the old Kilo Code extension. All of these need to be rebuilt in the new extension.
> **Note:** For webview-internal context menus (right-click inside the chat panel), see [Context Menus & Tooltips (Webview)](../chat-ui-features/context-menus-tooltips.md).
> For SCM commit message generation, see [Git Commit Message Generation](git-commit-message-generation.md).
> This document supersedes the old stub at [`code-actions.md`](code-actions.md).
---
## Overview
The old extension registered a comprehensive set of VS Code contributions:
- A **"Kilo Code" submenu** in the editor right-click context menu
- A **"Kilo Code" submenu** in the terminal right-click context menu
- A **CodeActionProvider** for lightbulb quick fixes
- **Keyboard shortcuts** for common actions
- **Prompt templates** (user-customizable) that turn captured context into agent task instructions
None of these exist yet in the rebuild.
---
## Editor Context Menus
Right-clicking in the editor shows a "Kilo Code" submenu with these commands:
| Command ID | Label | Captured Context | Behavior |
| -------------- | -------------- | ------------------------------------------------- | ------------------------------------------------------------------------ |
| `explainCode` | Explain Code | File path, selected text, line range | Starts an agent task with the EXPLAIN prompt |
| `fixCode` | Fix Code | File path, selected text, line range, diagnostics | Starts an agent task with the FIX prompt |
| `improveCode` | Improve Code | File path, selected text, line range | Starts an agent task with the IMPROVE prompt |
| `addToContext` | Add to Context | File path, selected text, line range | Injects formatted code block into chat input (does **not** start a task) |
All commands use the VS Code editor API to capture the active editor's file path (`document.uri`), current selection (`editor.selection`), and the selected text. `fixCode` additionally captures diagnostics from `vscode.languages.getDiagnostics()` for the selection range.
---
## Terminal Context Menus
Right-clicking in the terminal shows a "Kilo Code" submenu:
| Command ID | Label | Captured Context | Behavior |
| ------------------------ | --------------- | ----------------------------------- | ----------------------------------------------------- |
| `terminalAddToContext` | Add to Context | Terminal selection or recent buffer | Injects terminal output into chat input |
| `terminalFixCommand` | Fix Command | Last executed command + output | Starts an agent task with the TERMINAL_FIX prompt |
| `terminalExplainCommand` | Explain Command | Last executed command + output | Starts an agent task with the TERMINAL_EXPLAIN prompt |
Terminal context capture uses `vscode.window.activeTerminal` and the terminal selection API.
> See also [Terminal / Shell Integration](terminal-shell-integration.md) for the underlying terminal command execution and PTY management that these context menu actions depend on.
---
## Code Action Provider (Lightbulb Quick Fixes)
A `CodeActionProvider` is registered for all languages. When the user clicks the lightbulb or presses the quick fix shortcut:
| Condition | Actions shown |
| ------------------------------ | --------------------------------------------------- |
| Always | **Add to Kilo Code** → triggers `addToContext` |
| Diagnostics in selection range | **Fix with Kilo Code** → triggers `fixCode` |
| No diagnostics | **Explain with Kilo Code** → triggers `explainCode` |
| No diagnostics | **Improve with Kilo Code** → triggers `improveCode` |
Controlled by the `enableCodeActions` extension setting (default: `true`).
---
## Keyboard Shortcuts
| Shortcut (Mac / Win+Linux) | Command | Description |
| ------------------------------- | ------------------------- | ----------------------------- |
| `Cmd+Shift+A` / `Ctrl+Shift+A` | Focus chat input | Opens/focuses the chat panel |
| `Cmd+K Cmd+A` / `Ctrl+K Ctrl+A` | Add selection to context | Runs `addToContext` |
| `Cmd+Shift+G` / `Ctrl+Shift+G` | Generate terminal command | Starts TERMINAL_GENERATE task |
| `Cmd+Alt+A` / `Ctrl+Alt+A` | Toggle auto-approve | Toggles auto-approval setting |
---
## Prompt Templates
The old extension defined prompt templates in `support-prompt.ts` that format captured context into agent task instructions. Each template is user-customizable via extension settings.
| Template | Used by | Purpose |
| ------------------------- | ------------------------- | ------------------------------------------------------- |
| `EXPLAIN` | `explainCode` | Ask the agent to explain the selected code |
| `FIX` | `fixCode` | Ask the agent to fix code, including diagnostic details |
| `IMPROVE` | `improveCode` | Ask the agent to improve/refactor selected code |
| `ADD_TO_CONTEXT` | `addToContext` | Format a code block for injection into chat input |
| `TERMINAL_ADD_TO_CONTEXT` | `terminalAddToContext` | Format terminal output for injection into chat input |
| `TERMINAL_FIX` | `terminalFixCommand` | Ask the agent to fix a failed terminal command |
| `TERMINAL_EXPLAIN` | `terminalExplainCommand` | Ask the agent to explain a terminal command/output |
| `TERMINAL_GENERATE` | Generate terminal command | Ask the agent to generate a terminal command |
| `COMMIT_MESSAGE` | SCM integration | Generate a commit message (tracked separately) |
---
## Data Flow
There are two distinct patterns:
### 1. Action → Agent Task
Commands like `explainCode`, `fixCode`, `improveCode`, `terminalFixCommand`, and `terminalExplainCommand` follow this flow:
1. User triggers command (context menu, code action, or keybinding)
2. Extension captures context from VS Code APIs (editor selection, diagnostics, terminal buffer)
3. Extension fills in the prompt template with captured context
4. Extension sends the formatted prompt as a new message to the CLI session (creating a new task)
### 2. Action → Context Injection
Commands like `addToContext` and `terminalAddToContext` follow a different flow:
1. User triggers command
2. Extension captures context from VS Code APIs
3. Extension formats the context as a code block using the template
4. Extension posts a message to the webview to **set the chat input text** (not submit it)
5. User can review and edit before sending
---
## Implementation Notes for Rebuild
### `package.json` Contributions
Register in `contributes`:
- `submenus`: Define "Kilo Code" submenus for editor and terminal contexts
- `menus`: Register commands under `editor/context` and `terminal/context` menu groups
- `commands`: Register all command IDs with titles and icons
- `keybindings`: Register keyboard shortcuts with `key`, `mac`, and `when` clauses
> **Note:** [`package.json`](../../package.json) already has `contributes.commands` (8 commands) and `contributes.menus` (`view/title` + `editor/title` menus) registered. New commands, submenus, and keybindings should be **added alongside** the existing entries, not replace them.
### CodeActionProvider
- Register a `CodeActionProvider` for `*` (all languages)
- Check `vscode.languages.getDiagnostics()` to decide which actions to show
- Gate behind the `enableCodeActions` setting
- Return `CodeAction` instances with `command` set to the appropriate command ID
### Editor Context Capture
- `vscode.window.activeTextEditor` for file path, selection, document
- `editor.document.getText(selection)` for selected text
- `selection.start.line` / `selection.end.line` for line range
- `vscode.languages.getDiagnostics(document.uri)` filtered to selection range for `fixCode`
### Terminal Context Capture
- `vscode.window.activeTerminal` for the active terminal
- Terminal selection API for selected text in terminal
- Shell integration API for last command and its output (where available)
### Prompt Templates
Need an equivalent template system in the extension. Options:
- Hardcode templates with settings overrides (like the old extension)
- Delegate prompt construction to the CLI (if it supports parameterized task creation)
### "Add to Context" Pattern
The webview needs to handle an incoming message that **sets the chat input text** without submitting it. This requires:
- A new message type (e.g., `SetChatInput`) in the extension→webview protocol
- The `PromptInput` component to accept externally-set text
---
## Prompt Templates (Full Text)
Below are the exact prompt templates from the old extension's `src/shared/support-prompt.ts`. Each template uses `${variable}` interpolation. These are the defaults; users can customize any template via the `customSupportPrompts` extension setting.
> **Note:** The `diagnosticText` variable used in the FIX template is auto-generated from VS Code diagnostics using the format `- [source] message (code)`.
### EXPLAIN
**Variables:** `filePath`, `startLine`, `endLine`, `userInput`, `selectedText`
```
Explain the following code from file path ${filePath}:${startLine}-${endLine}
${userInput}
\`\`\`
${selectedText}
\`\`\`
Please provide a clear and concise explanation of what this code does, including:
1. The purpose and functionality
2. Key components and their interactions
3. Important patterns or techniques used
```
### FIX
**Variables:** `filePath`, `startLine`, `endLine`, `diagnosticText`, `userInput`, `selectedText`
```
Fix any issues in the following code from file path ${filePath}:${startLine}-${endLine}
${diagnosticText}
${userInput}
\`\`\`
${selectedText}
\`\`\`
Please:
1. Address all detected problems listed above (if any)
2. Identify any other potential bugs or issues
3. Provide corrected code
4. Explain what was fixed and why
```
### IMPROVE
**Variables:** `filePath`, `startLine`, `endLine`, `userInput`, `selectedText`
```
Improve the following code from file path ${filePath}:${startLine}-${endLine}
${userInput}
\`\`\`
${selectedText}
\`\`\`
Please suggest improvements for:
1. Code readability and maintainability
2. Performance optimization
3. Best practices and patterns
4. Error handling and edge cases
Provide the improved code along with explanations for each enhancement.
```
### ADD_TO_CONTEXT
**Variables:** `filePath`, `startLine`, `endLine`, `selectedText`
```
${filePath}:${startLine}-${endLine}
\`\`\`
${selectedText}
\`\`\`
```
### TERMINAL_ADD_TO_CONTEXT
**Variables:** `userInput`, `terminalContent`
```
${userInput}
Terminal output:
\`\`\`
${terminalContent}
\`\`\`
```
### TERMINAL_FIX
**Variables:** `userInput`, `terminalContent`
```
${userInput}
Fix this terminal command:
\`\`\`
${terminalContent}
\`\`\`
Please:
1. Identify any issues in the command
2. Provide the corrected command
3. Explain what was fixed and why
```
### TERMINAL_EXPLAIN
**Variables:** `userInput`, `terminalContent`
```
${userInput}
Explain this terminal command:
\`\`\`
${terminalContent}
\`\`\`
Please provide:
1. What the command does
2. Explanation of each part/flag
3. Expected output and behavior
```
### TERMINAL*GENERATE *(Kilo-specific addition)\_
**Variables:** `userInput`, `operatingSystem`, `currentDirectory`, `shell`
```
Generate a terminal command based on this description: "${userInput}"
Context:
- Operating System: ${operatingSystem}
- Current Directory: ${currentDirectory}
- Shell: ${shell}
Requirements:
1. Generate ONLY the command, no explanations or formatting
2. Ensure the command is safe and appropriate
3. Use common command-line tools and best practices
4. Consider the current working directory context
5. Return only the raw command that can be executed directly
```
### ENHANCE
**Variables:** `userInput`
```
Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):
${userInput}
```
### CONDENSE
**Variables:** none (system prompt for conversation summary)
```
Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing with the conversation and supporting any continuing tasks.
Your summary should be structured as follows:
Context: The context to continue the conversation with. If applicable based on the current task, this should include:
1. Previous Conversation: High level details about what was discussed throughout the entire conversation with the user. This should be written to allow someone to be able to follow the general overarching conversation flow.
2. Current Work: Describe in detail what was being worked on prior to this request to summarize the conversation. Pay special attention to the more recent messages in the conversation.
3. Key Technical Concepts: List all important technical concepts, technologies, coding conventions, and frameworks discussed, which might be relevant for continuing with this work.
4. Relevant Files and Code: If applicable, enumerate specific files and code sections examined, modified, or created for the task continuation. Pay special attention to the most recent messages and changes.
5. Problem Solving: Document problems solved thus far and any ongoing troubleshooting efforts.
6. Pending Tasks and Next Steps: Outline all pending tasks that you have explicitly been asked to work on, as well as list the next steps you will take for all outstanding work, if applicable. Include code snippets where they add clarity. For any next steps, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no information loss in context between tasks.
Example summary structure:
1. Previous Conversation:
[Detailed description]
2. Current Work:
[Detailed description]
3. Key Technical Concepts:
- [Concept 1]
- [Concept 2]
- [...]
4. Relevant Files and Code:
- [File Name 1]
- [Summary of why this file is important]
- [Summary of the changes made to this file, if any]
- [Important Code Snippet]
- [File Name 2]
- [Important Code Snippet]
- [...]
5. Problem Solving:
[Detailed description]
6. Pending Tasks and Next Steps:
- [Task 1 details & next steps]
- [Task 2 details & next steps]
- [...]
Output only the summary of the conversation so far, without any additional commentary or explanation.
```
### COMMIT*MESSAGE *(Kilo-specific addition, tracked separately in [git-commit-message-generation.md](git-commit-message-generation.md))\_
**Variables:** `customInstructions`, `gitContext`
````
# Conventional Commit Message Generator
## System Instructions
You are an expert Git commit message generator that creates conventional commit messages based on staged changes. Analyze the provided git diff output and generate appropriate conventional commit messages following the specification.
${customInstructions}
## CRITICAL: Commit Message Output Rules
- DO NOT include any internal status indicators or bracketed metadata (e.g. "[Status: Active]", "[Context: Missing]")
- DO NOT include any task-specific formatting or artifacts from other rules
- ONLY Generate a clean conventional commit message as specified below
${gitContext}
## Conventional Commits Format
Generate commit messages following this exact structure:
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
### Core Types (Required)
- **feat**: New feature or functionality (MINOR version bump)
- **fix**: Bug fix or error correction (PATCH version bump)
### Additional Types (Extended)
- **docs**: Documentation changes only
- **style**: Code style changes (whitespace, formatting, semicolons, etc.)
- **refactor**: Code refactoring without feature changes or bug fixes
- **perf**: Performance improvements
- **test**: Adding or fixing tests
- **build**: Build system or external dependency changes
- **ci**: CI/CD configuration changes
- **chore**: Maintenance tasks, tooling changes
- **revert**: Reverting previous commits
### Scope Guidelines
- Use parentheses: `feat(api):`, `fix(ui):`
- Common scopes: `api`, `ui`, `auth`, `db`, `config`, `deps`, `docs`
- For monorepos: package or module names
- Keep scope concise and lowercase
### Description Rules
- Use imperative mood ("add" not "added" or "adds")
- Start with lowercase letter
- No period at the end
- Maximum 50 characters
- Be concise but descriptive
### Body Guidelines (Optional)
- Start one blank line after description
- Explain the "what" and "why", not the "how"
- Wrap at 72 characters per line
- Use for complex changes requiring explanation
### Footer Guidelines (Optional)
- Start one blank line after body
- **Breaking Changes**: `BREAKING CHANGE: description`
## Analysis Instructions
When analyzing staged changes:
1. Determine Primary Type based on the nature of changes
2. Identify Scope from modified directories or modules
3. Craft Description focusing on the most significant change
4. Determine if there are Breaking Changes
5. For complex changes, include a detailed body explaining what and why
6. Add appropriate footers for issue references or breaking changes
For significant changes, include a detailed body explaining the changes.
Return ONLY the commit message in the conventional format, nothing else.
````
### NEW_TASK
**Variables:** `userInput`
```
${userInput}
```
---
### Already Done / Tracked Elsewhere
- **View title bar buttons**: ✅ Done ([#181](https://github.com/Kilo-Org/kilo/issues/181))
- **SCM commit message generation**: Tracked in [git-commit-message-generation.md](git-commit-message-generation.md)
@@ -72,41 +72,41 @@ The rebuild has a working foundation:
## Non-Agent Feature Parity
| Feature | Status | Details | Backend | Priority |
| ------------------------------------------------------------------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -------- |
| Agent Manager | ✅ Done | `AgentManagerProvider` opens editor panel, creates git worktrees for parallel sessions, full `createWorktreeSession` lifecycle (worktree → session → first message). `WorktreeManager` handles `git worktree add`, metadata, and discovery. `WorktreeSelector` in PromptInput toggles local/worktree mode. [#178](https://github.com/Kilo-Org/kilo/issues/178) | Extension orchestrates multiple CLI sessions | P1 |
| [Authentication & Enterprise](non-agent-features/authentication-organization-enterprise-enforcement.md) | 🔨 Partial | Device auth flow works, organization switching implemented. Missing: org feature flags, MDM policy enforcement. [#160](https://github.com/Kilo-Org/kilo/issues/160) | CLI handles its auth; extension handles org/MDM | P1 |
| [Auto-Purge](non-agent-features/auto-purge.md) | ❌ Not started | No scheduled cleanup of old session/task storage. | Extension-side (storage ownership TBD) | P3 |
| Autocomplete / Ghost | ✅ Done | `AutocompleteInlineCompletionProvider` implements `vscode.InlineCompletionItemProvider` with FIM completions via Kilo Gateway. Includes debouncing, LRU cache, tree-sitter context, bracket matching, and stream-based completion. `AutocompleteServiceManager` registers/unregisters the provider. [#164](https://github.com/Kilo-Org/kilo/issues/164) | Extension-side (VS Code InlineCompletionProvider) | P1 |
| [Browser Automation & URL Ingestion](non-agent-features/browser-automation-url-ingestion.md) | 🔨 Partial | Playwright MCP integration implemented: settings toggle, BrowserAutomationService, CLI MCP hub registration, BrowserTab settings UI. Missing: URL-to-markdown ingestion, screenshot viewing in chat. | CLI MCP hub (POST /mcp); extension manages lifecycle | P3 |
| [Checkpoints](non-agent-features/checkpoints.md) | ❌ Not started | No shadow git repo, per-task snapshots, restore UI, or diff viewing. Settings tab is a stub. | CLI (partial: session undo/redo); extension for git snapshots | P1 |
| [Cloud Task Support](non-agent-features/cloud-task-support.md) | ❌ Not started | No cloud sync for tasks across devices. [#168](https://github.com/Kilo-Org/kilo/issues/168) | Kilo cloud API + CLI; extension provides UI | P2 |
| [Code Actions](non-agent-features/code-actions.md) | ❌ Not started | No VS Code lightbulb/context menu integrations (explain, fix, improve). | Extension-side (VS Code CodeActionProvider) | P2 |
| [Code Reviews](non-agent-features/code-reviews.md) | ❌ Not started | No local review mode or automated AI review of uncommitted/branch changes. | CLI (partial); extension for VS Code review UX | P2 |
| [Codebase Indexing & Semantic Search](non-agent-features/codebase-indexing-semantic-search.md) | ❌ Not started | No vector indexing, semantic search, or embeddings infrastructure. | CLI has grep/glob endpoints; semantic indexing is extension or cloud | P2 |
| [Contribution Tracking](non-agent-features/contribution-tracking.md) | ❌ Not started | No AI attribution tracking, line fingerprinting, or reporting. | Extension-side | P3 |
| [Custom Commands](non-agent-features/custom-command-system.md) | ❌ Not started | No slash commands, project-level command discovery, or YAML frontmatter support. | CLI has custom commands; extension provides UI entry points | P2 |
| [Deploy & Secure Surfaces](non-agent-features/deploy-and-secure-surfaces.md) | ❌ Not started | No deploy workflows, managed indexing UI, or security review surfaces. | Extension-side | P3 |
| [Fast Edits](non-agent-features/fast-edits.md) | ❌ Not started | No fast edit mode for quick inline code changes. | CLI fast-edit runtime; extension provides UI | P2 |
| [Git Commit Message Generation](non-agent-features/git-commit-message-generation.md) | ❌ Not started | No AI commit message generation or VS Code Source Control integration. [#165](https://github.com/Kilo-Org/kilo/issues/165) | Extension-side (VS Code Git API) | P2 |
| [Integrations](non-agent-features/integrations.md) | ❌ Not started | No external system integrations (GitHub, etc.) beyond basic auth. | CLI plugin system (partial); extension for IDE hooks | P3 |
| Kilo Gateway | ✅ Done | Kilo Gateway (`KILO_GATEWAY_ID = "kilo"`) is the default provider, listed first in ModelSelector via `PROVIDER_ORDER`. Login/logout/org switching fully implemented in `KiloProvider.ts` and `ProfileView.tsx`. Default model selection in ProvidersTab. [#176](https://github.com/Kilo-Org/kilo/issues/176) | CLI handles gateway connection; extension provides config UI | P0 |
| Localization | ✅ Done | Full i18n system with 16 locales in [`i18n/`](../webview-ui/src/i18n/), three-layer dict merging, locale auto-detection, LanguageTab settings UI with locale selector. | Extension + webview; CLI locale mapping needed | P3 |
| [Marketplace](non-agent-features/marketplace.md) | 🔨 Partial | Placeholder view exists but is non-functional. No catalog, install, or update capabilities. [#169](https://github.com/Kilo-Org/kilo/issues/169) | Extension-side | P2 |
| [MCP & MCP Hub](non-agent-features/mcp-and-mcp-hub.md) | 🔨 Partial | MCP types and HTTP client methods added (getMcpStatus, addMcpServer, connectMcpServer, disconnectMcpServer). Used by BrowserAutomationService. Missing: general MCP configuration UI, server management, tool allowlisting, connection status display. | CLI owns MCP lifecycle; extension provides config UI | P1 |
| Mode Switcher | ✅ Done | ModeSwitcher.tsx implements popover-based agent/mode selector with descriptions, persisted via session.selectAgent(). Integrated into PromptInput. [#162](https://github.com/Kilo-Org/kilo/issues/162) | CLI manages modes; extension provides switcher UI | P2 |
| Model Switcher | ✅ Done | ModelSelector.tsx implements popover-based selector with search/filter, keyboard navigation, provider grouping, free model tags. Integrated into PromptInput. [#163](https://github.com/Kilo-Org/kilo/issues/163) | CLI provides model list; extension provides switcher UI | P1 |
| Provider Configuration | ✅ Done | `ProvidersTab` implements default/small model selection (backed by `ConfigProvider` + `PATCH /global/config`), disabled-providers list, and enabled-providers allowlist. Provider context wired into ModelSelector. [#175](https://github.com/Kilo-Org/kilo/issues/175) | CLI manages providers; extension provides config UI | P1 |
| [Repository Initialization](non-agent-features/repository-initialization.md) | ❌ Not started | No /init command support for setting up agentic engineering. [#174](https://github.com/Kilo-Org/kilo/issues/174) | CLI /init endpoint; extension provides UI trigger | P3 |
| [Rules & Workflows](non-agent-features/rules-and-workflows.md) | ❌ Not started | No rules or workflow management UI. [#173](https://github.com/Kilo-Org/kilo/issues/173) | CLI owns rules runtime; extension provides management UI | P3 |
| [Search & Repo Scanning](non-agent-features/search-and-repo-scanning-infrastructure.md) | ❌ Not started | No search infrastructure beyond CLI grep/glob. | CLI has grep/glob; extension may add UI | P2 |
| [Settings Sync](non-agent-features/settings-sync-integration.md) | ❌ Not started | No VS Code Settings Sync allowlist registration. Settings tabs are all stubs. | Extension-side (VS Code API) | P3 |
| [Settings UI](non-agent-features/settings-ui.md) | 🔨 Partial | 14-tab settings implemented: Providers (model selection + allowlists), AgentBehaviour (MCP read-only, rules, skills), AutoApprove (per-tool dropdowns), Browser, Autocomplete, Display, Notifications, Context, Terminal, Prompts, Experimental, Language, AboutKiloCode. Missing: Workflows subtab (placeholder). [#170](https://github.com/Kilo-Org/kilo/issues/170) | CLI exposes config; extension provides settings forms | P1 |
| [Skills System](non-agent-features/skills-system.md) | ❌ Not started | No skill discovery, management, or hot-reload in extension. | CLI has skills runtime; extension provides packaging/UI | P2 |
| [Speech-to-Text](non-agent-features/speech-to-text.md) | ❌ Not started | No voice input or streaming STT. | Webview (mic capture); CLI-compatible STT optional | P3 |
| Task History | ✅ Done | `SessionList` uses kilo-ui `List` with search (filterKeys=["title"]), date grouping (Today/Yesterday/This Week/This Month/Older), relative timestamps, inline rename, and delete with confirmation. [#167](https://github.com/Kilo-Org/kilo/issues/167) | CLI session storage; extension provides history UI | P1 |
| Telemetry | ✅ Done | `TelemetryProxy` singleton POSTs events to `/telemetry/capture` respecting `vscode.env.isTelemetryEnabled`. Webview `captureTelemetryEvent()` posts to extension which forwards to `TelemetryProxy`. Both sides wired end-to-end. | Extension-side (PostHog + kilo-telemetry) | P1 |
| [Terminal / Shell Integration](non-agent-features/terminal-shell-integration.md) | ❌ Not started | No VS Code terminal integration for command execution display, exit code tracking, or working directory changes. | CLI executes commands; extension provides terminal UX | P1 |
| Feature | Status | Details | Backend | Priority |
| ------------------------------------------------------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -------- |
| Agent Manager | ✅ Done | `AgentManagerProvider` opens editor panel, creates git worktrees for parallel sessions, full `createWorktreeSession` lifecycle (worktree → session → first message). `WorktreeManager` handles `git worktree add`, metadata, and discovery. `WorktreeSelector` in PromptInput toggles local/worktree mode. [#178](https://github.com/Kilo-Org/kilo/issues/178) | Extension orchestrates multiple CLI sessions | P1 |
| [Authentication & Enterprise](non-agent-features/authentication-organization-enterprise-enforcement.md) | 🔨 Partial | Device auth flow works, organization switching implemented. Missing: org feature flags, MDM policy enforcement. [#160](https://github.com/Kilo-Org/kilo/issues/160) | CLI handles its auth; extension handles org/MDM | P1 |
| [Auto-Purge](non-agent-features/auto-purge.md) | ❌ Not started | No scheduled cleanup of old session/task storage. | Extension-side (storage ownership TBD) | P3 |
| Autocomplete / Ghost | ✅ Done | `AutocompleteInlineCompletionProvider` implements `vscode.InlineCompletionItemProvider` with FIM completions via Kilo Gateway. Includes debouncing, LRU cache, tree-sitter context, bracket matching, and stream-based completion. `AutocompleteServiceManager` registers/unregisters the provider. [#164](https://github.com/Kilo-Org/kilo/issues/164) | Extension-side (VS Code InlineCompletionProvider) | P1 |
| [Browser Automation & URL Ingestion](non-agent-features/browser-automation-url-ingestion.md) | 🔨 Partial | Playwright MCP integration implemented: settings toggle, BrowserAutomationService, CLI MCP hub registration, BrowserTab settings UI. Missing: URL-to-markdown ingestion, screenshot viewing in chat. | CLI MCP hub (POST /mcp); extension manages lifecycle | P3 |
| [Checkpoints](non-agent-features/checkpoints.md) | ❌ Not started | No shadow git repo, per-task snapshots, restore UI, or diff viewing. Settings tab is a stub. | CLI (partial: session undo/redo); extension for git snapshots | P1 |
| [Cloud Task Support](non-agent-features/cloud-task-support.md) | ❌ Not started | No cloud sync for tasks across devices. [#168](https://github.com/Kilo-Org/kilo/issues/168) | Kilo cloud API + CLI; extension provides UI | P2 |
| [Code Actions & Editor Menus](non-agent-features/editor-context-menus-and-code-actions.md) | 🔨 Partial | Editor/terminal right-click submenus, CodeActionProvider lightbulb, keyboard shortcuts, and prompt templates implemented. Terminal content capture is a stub (needs shell integration API). Missing: custom prompt overrides via settings. | Extension-side (VS Code CodeActionProvider + menus + keybindings) | P1 |
| [Code Reviews](non-agent-features/code-reviews.md) | ❌ Not started | No local review mode or automated AI review of uncommitted/branch changes. | CLI (partial); extension for VS Code review UX | P2 |
| [Codebase Indexing & Semantic Search](non-agent-features/codebase-indexing-semantic-search.md) | ❌ Not started | No vector indexing, semantic search, or embeddings infrastructure. | CLI has grep/glob endpoints; semantic indexing is extension or cloud | P2 |
| [Contribution Tracking](non-agent-features/contribution-tracking.md) | ❌ Not started | No AI attribution tracking, line fingerprinting, or reporting. | Extension-side | P3 |
| [Custom Commands](non-agent-features/custom-command-system.md) | ❌ Not started | No slash commands, project-level command discovery, or YAML frontmatter support. | CLI has custom commands; extension provides UI entry points | P2 |
| [Deploy & Secure Surfaces](non-agent-features/deploy-and-secure-surfaces.md) | ❌ Not started | No deploy workflows, managed indexing UI, or security review surfaces. | Extension-side | P3 |
| [Fast Edits](non-agent-features/fast-edits.md) | ❌ Not started | No fast edit mode for quick inline code changes. | CLI fast-edit runtime; extension provides UI | P2 |
| [Git Commit Message Generation](non-agent-features/git-commit-message-generation.md) | ❌ Not started | No AI commit message generation or VS Code Source Control integration. [#165](https://github.com/Kilo-Org/kilo/issues/165) | Extension-side (VS Code Git API) | P2 |
| [Integrations](non-agent-features/integrations.md) | ❌ Not started | No external system integrations (GitHub, etc.) beyond basic auth. | CLI plugin system (partial); extension for IDE hooks | P3 |
| Kilo Gateway | ✅ Done | Kilo Gateway (`KILO_GATEWAY_ID = "kilo"`) is the default provider, listed first in ModelSelector via `PROVIDER_ORDER`. Login/logout/org switching fully implemented in `KiloProvider.ts` and `ProfileView.tsx`. Default model selection in ProvidersTab. [#176](https://github.com/Kilo-Org/kilo/issues/176) | CLI handles gateway connection; extension provides config UI | P0 |
| Localization | ✅ Done | Full i18n system with 16 locales in [`i18n/`](../webview-ui/src/i18n/), three-layer dict merging, locale auto-detection, LanguageTab settings UI with locale selector. | Extension + webview; CLI locale mapping needed | P3 |
| [Marketplace](non-agent-features/marketplace.md) | 🔨 Partial | Placeholder view exists but is non-functional. No catalog, install, or update capabilities. [#169](https://github.com/Kilo-Org/kilo/issues/169) | Extension-side | P2 |
| [MCP & MCP Hub](non-agent-features/mcp-and-mcp-hub.md) | 🔨 Partial | MCP types and HTTP client methods added (getMcpStatus, addMcpServer, connectMcpServer, disconnectMcpServer). Used by BrowserAutomationService. Missing: general MCP configuration UI, server management, tool allowlisting, connection status display. | CLI owns MCP lifecycle; extension provides config UI | P1 |
| [Mode Switcher](non-agent-features/mode-switcher.md) | ✅ Done | ModeSwitcher.tsx implements popover-based agent/mode selector with descriptions, persisted via session.selectAgent(). Integrated into PromptInput. [#162](https://github.com/Kilo-Org/kilo/issues/162) | CLI manages modes; extension provides switcher UI | P2 |
| [Model Switcher](non-agent-features/model-switcher.md) | ✅ Done | ModelSelector.tsx implements popover-based selector with search/filter, keyboard navigation, provider grouping, free model tags. Integrated into PromptInput. [#163](https://github.com/Kilo-Org/kilo/issues/163) | CLI provides model list; extension provides switcher UI | P1 |
| [Provider Configuration](non-agent-features/provider-configuration.md) | 🔨 Partial | Provider context fetches and exposes provider data, connected providers, defaults, model lists — wired into ModelSelector. ProvidersTab UI still a stub. [#175](https://github.com/Kilo-Org/kilo/issues/175) | CLI manages providers; extension provides config UI | P1 |
| [Repository Initialization](non-agent-features/repository-initialization.md) | ❌ Not started | No /init command support for setting up agentic engineering. [#174](https://github.com/Kilo-Org/kilo/issues/174) | CLI /init endpoint; extension provides UI trigger | P3 |
| [Rules & Workflows](non-agent-features/rules-and-workflows.md) | ❌ Not started | No rules or workflow management UI. [#173](https://github.com/Kilo-Org/kilo/issues/173) | CLI owns rules runtime; extension provides management UI | P3 |
| [Search & Repo Scanning](non-agent-features/search-and-repo-scanning-infrastructure.md) | ❌ Not started | No search infrastructure beyond CLI grep/glob. | CLI has grep/glob; extension may add UI | P2 |
| [Settings Sync](non-agent-features/settings-sync-integration.md) | ❌ Not started | No VS Code Settings Sync allowlist registration. Settings tabs are all stubs. | Extension-side (VS Code API) | P3 |
| [Settings UI](non-agent-features/settings-ui.md) | 🔨 Partial | 15-tab settings shell exists. BrowserTab has real settings controls (enable/disable, system Chrome, headless toggles). LanguageTab has working locale selector. Remaining 13 tabs are stubs. [#170](https://github.com/Kilo-Org/kilo/issues/170) | CLI exposes config; extension provides settings forms | P1 |
| [Skills System](non-agent-features/skills-system.md) | ❌ Not started | No skill discovery, management, or hot-reload in extension. | CLI has skills runtime; extension provides packaging/UI | P2 |
| [Speech-to-Text](non-agent-features/speech-to-text.md) | ❌ Not started | No voice input or streaming STT. | Webview (mic capture); CLI-compatible STT optional | P3 |
| [Task History](non-agent-features/task-history.md) | 🔨 Partial | Session list exists but lacks search, metadata, and full persistence. [#167](https://github.com/Kilo-Org/kilo/issues/167) | CLI session storage; extension provides history UI | P1 |
| [Telemetry](non-agent-features/telemetry.md) | ❌ Not started | Dual-layer telemetry (PostHog Node server-side + PostHog JS client-side) for extension usage, errors, LLM completions, and AI interactions. Includes privacy controls, typed events, and structured error tracking. See detailed plan. | Extension-side (PostHog + kilo-telemetry) | P1 |
| [Terminal / Shell Integration](non-agent-features/terminal-shell-integration.md) | ❌ Not started | No VS Code terminal integration for command execution display, exit code tracking, or working directory changes. | CLI executes commands; extension provides terminal UX | P1 |
---
+212 -22
View File
@@ -2,7 +2,7 @@
"name": "kilo-code",
"displayName": "Kilo Code",
"description": "Kilo Code AI Agent & Autocomplete",
"version": "7.0.23",
"version": "7.0.24",
"publisher": "kilocode",
"repository": {
"type": "git",
@@ -100,32 +100,97 @@
"command": "kilo-code.new.agentManager.nextTab",
"title": "Agent Manager: Next Tab",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.showTerminal",
"title": "Agent Manager: Focus Terminal",
"category": "Kilo Code",
"icon": "$(terminal)"
},
{
"command": "kilo-code.new.agentManager.focusPanel",
"title": "Agent Manager: Focus Panel",
"category": "Kilo Code",
"icon": "$(circuit-board)"
},
{
"command": "kilo-code.new.agentManager.newTab",
"title": "Agent Manager: New Tab",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.closeTab",
"title": "Agent Manager: Close Tab",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.newWorktree",
"title": "Agent Manager: New Worktree",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.closeWorktree",
"title": "Agent Manager: Close Worktree",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.generateCommitMessage",
"title": "Generate Commit Message",
"category": "Kilo Code (NEW)",
"icon": {
"light": "assets/icons/kilo-light.svg",
"dark": "assets/icons/kilo-dark.svg"
}
},
{
"command": "kilo-code.new.explainCode",
"title": "Explain Code",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.fixCode",
"title": "Fix Code",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.improveCode",
"title": "Improve Code",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.addToContext",
"title": "Add to Context",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.terminalAddToContext",
"title": "Add Terminal Content to Context",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.terminalFixCommand",
"title": "Fix This Command",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.terminalExplainCommand",
"title": "Explain This Command",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.focusChatInput",
"title": "Focus Chat Input",
"category": "Kilo Code"
}
],
"keybindings": [
"submenus": [
{
"command": "kilo-code.new.agentManager.previousSession",
"key": "ctrl+up",
"mac": "cmd+up",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
"id": "kilo-code.new.editorContextMenu",
"label": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.nextSession",
"key": "ctrl+down",
"mac": "cmd+down",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.previousTab",
"key": "ctrl+left",
"mac": "cmd+left",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.nextTab",
"key": "ctrl+right",
"mac": "cmd+right",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
"id": "kilo-code.new.terminalContextMenu",
"label": "Kilo Code"
}
],
"menus": {
@@ -161,14 +226,138 @@
"when": "view == kilo-code.new.sidebarView"
}
],
"scm/title": [
{
"command": "kilo-code.new.generateCommitMessage",
"group": "navigation",
"when": "scmProvider == git"
}
],
"editor/title": [
{
"command": "kilo-code.new.openInTab",
"group": "navigation",
"when": "true"
}
],
"editor/context": [
{
"submenu": "kilo-code.new.editorContextMenu",
"group": "1_kilocode"
}
],
"kilo-code.new.editorContextMenu": [
{
"command": "kilo-code.new.explainCode",
"group": "1_actions@1"
},
{
"command": "kilo-code.new.fixCode",
"group": "1_actions@2"
},
{
"command": "kilo-code.new.improveCode",
"group": "1_actions@3"
},
{
"command": "kilo-code.new.addToContext",
"group": "1_actions@4"
}
],
"terminal/context": [
{
"submenu": "kilo-code.new.terminalContextMenu",
"group": "2_kilocode"
}
],
"kilo-code.new.terminalContextMenu": [
{
"command": "kilo-code.new.terminalAddToContext",
"group": "1_actions@1"
},
{
"command": "kilo-code.new.terminalFixCommand",
"group": "1_actions@2"
},
{
"command": "kilo-code.new.terminalExplainCommand",
"group": "1_actions@3"
}
]
},
"keybindings": [
{
"command": "kilo-code.new.focusChatInput",
"key": "ctrl+shift+a",
"mac": "cmd+shift+a"
},
{
"command": "kilo-code.new.addToContext",
"key": "ctrl+k ctrl+a",
"mac": "cmd+k cmd+a",
"when": "editorTextFocus && editorHasSelection"
},
{
"command": "kilo-code.new.agentManager.previousSession",
"key": "ctrl+up",
"mac": "cmd+up",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.nextSession",
"key": "ctrl+down",
"mac": "cmd+down",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.previousTab",
"key": "ctrl+left",
"mac": "cmd+left",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.nextTab",
"key": "ctrl+right",
"mac": "cmd+right",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.showTerminal",
"key": "ctrl+/",
"mac": "cmd+/",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.focusPanel",
"key": "ctrl+.",
"mac": "cmd+.",
"when": "terminalFocus && kilo-code.agentTerminalFocus"
},
{
"command": "kilo-code.new.agentManager.newTab",
"key": "ctrl+t",
"mac": "cmd+t",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.closeTab",
"key": "ctrl+w",
"mac": "cmd+w",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.newWorktree",
"key": "ctrl+n",
"mac": "cmd+n",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.closeWorktree",
"key": "ctrl+shift+w",
"mac": "cmd+shift+w",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
}
],
"configuration": {
"title": "Kilo Code",
"properties": {
@@ -331,6 +520,7 @@
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1",
"qrcode": "^1.5.4",
"ts-morph": "27.0.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0"
},
@@ -4,6 +4,7 @@ import { KiloProvider } from "../KiloProvider"
import { buildWebviewHtml } from "../utils"
import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager"
import { WorktreeStateManager } from "./WorktreeStateManager"
import { SessionTerminalManager } from "./SessionTerminalManager"
/**
* AgentManagerProvider opens the Agent Manager panel.
@@ -21,12 +22,16 @@ export class AgentManagerProvider implements vscode.Disposable {
private outputChannel: vscode.OutputChannel
private worktrees: WorktreeManager | undefined
private state: WorktreeStateManager | undefined
private terminalManager: SessionTerminalManager
constructor(
private readonly extensionUri: vscode.Uri,
private readonly connectionService: KiloConnectionService,
) {
this.outputChannel = vscode.window.createOutputChannel("Kilo Agent Manager")
this.terminalManager = new SessionTerminalManager((msg) =>
this.outputChannel.appendLine(`[SessionTerminal] ${msg}`),
)
}
private log(...args: unknown[]) {
@@ -124,11 +129,20 @@ export class AgentManagerProvider implements vscode.Disposable {
return this.onAddSessionToWorktree(msg.worktreeId)
if (type === "agentManager.closeSession" && typeof msg.sessionId === "string")
return this.onCloseSession(msg.sessionId)
if (type === "agentManager.showTerminal" && typeof msg.sessionId === "string") {
this.terminalManager.showTerminal(msg.sessionId, this.state)
return null
}
if (type === "agentManager.requestRepoInfo") {
void this.sendRepoInfo()
return null
}
// When switching sessions, show existing terminal if one is open
if (type === "loadMessages" && typeof msg.sessionID === "string") {
this.terminalManager.showExisting(msg.sessionID)
}
// After clearSession, re-register worktree sessions so SSE events keep flowing
if (type === "clearSession") {
void Promise.resolve().then(() => {
@@ -308,11 +322,7 @@ export class AgentManagerProvider implements vscode.Disposable {
try {
client = this.connectionService.getHttpClient()
} catch {
this.postToWebview({
type: "agentManager.worktreeSetup",
status: "error",
message: "Not connected to CLI backend",
})
this.postToWebview({ type: "error", message: "Not connected to CLI backend" })
return null
}
@@ -338,11 +348,9 @@ export class AgentManagerProvider implements vscode.Disposable {
this.registerWorktreeSession(session.id, worktree.path)
this.pushState()
this.postToWebview({
type: "agentManager.worktreeSetup",
status: "ready",
message: "Session created",
type: "agentManager.sessionAdded",
sessionId: session.id,
branch: worktree.branch,
worktreeId,
})
if (this.provider) {
@@ -449,11 +457,29 @@ export class AgentManagerProvider implements vscode.Disposable {
})
}
/**
* Show terminal for the currently active session (triggered by keyboard shortcut).
* Posts an action to the webview which will respond with the session ID.
*/
public showTerminalForCurrentSession(): void {
this.postToWebview({ type: "action", action: "showTerminal" })
}
/**
* Reveal the Agent Manager panel and focus the prompt input.
* Used for the keyboard shortcut to switch back from terminal.
*/
public focusPanel(): void {
if (!this.panel) return
this.panel.reveal(vscode.ViewColumn.One, false)
}
public postMessage(message: unknown): void {
this.panel?.webview.postMessage(message)
}
public dispose(): void {
this.terminalManager.dispose()
this.provider?.dispose()
this.panel?.dispose()
this.outputChannel.dispose()
@@ -0,0 +1,135 @@
import * as vscode from "vscode"
import type { WorktreeStateManager } from "./WorktreeStateManager"
/**
* Manages VS Code terminals for agent manager sessions.
* Each session can have an associated terminal that opens in the session's worktree directory,
* or the main workspace folder for local sessions.
*/
export class SessionTerminalManager {
private terminals = new Map<string, { terminal: vscode.Terminal; cwd: string }>()
private disposables: vscode.Disposable[] = []
constructor(private log: (msg: string) => void) {
this.disposables.push(
vscode.window.onDidCloseTerminal((terminal) => {
for (const [sessionId, entry] of this.terminals) {
if (entry.terminal !== terminal) continue
this.terminals.delete(sessionId)
this.log(`Removed terminal mapping for session ${sessionId} (terminal closed)`)
break
}
this.updateContextKey()
}),
vscode.window.onDidChangeActiveTerminal((terminal) => {
const managed = terminal ? this.isManaged(terminal) : false
vscode.commands.executeCommand("setContext", "kilo-code.agentTerminalFocus", managed)
}),
)
}
/**
* Show (or create) a terminal for the given session.
* Resolves CWD from the worktree state, falling back to workspace root.
*/
showTerminal(sessionId: string, state: WorktreeStateManager | undefined): void {
// If terminal already exists, just focus it
if (this.showExisting(sessionId, false)) return
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
const worktreePath = state?.directoryFor(sessionId)
const cwd = worktreePath ?? workspacePath
if (!cwd) {
this.log(`showTerminal: no cwd resolved for session ${sessionId}`)
vscode.window.showWarningMessage("No workspace folder open")
return
}
const session = state?.getSession(sessionId)
const worktree = session?.worktreeId ? state?.getWorktree(session.worktreeId) : undefined
const name = worktree ? `Agent: ${worktree.branch}` : "Agent: local"
this.showOrCreate(sessionId, cwd, name)
}
/**
* Show the terminal for a session if it already exists (used when switching sessions).
* Returns true if the terminal was shown, false if no terminal exists for the session.
* Pass preserveFocus=true to keep focus on the current editor (default for session switching).
*/
showExisting(sessionId: string, preserveFocus = true): boolean {
const entry = this.terminals.get(sessionId)
if (!entry) return false
if (entry.terminal.exitStatus !== undefined) {
this.terminals.delete(sessionId)
this.log(`showExisting: terminal exited for session ${sessionId}, clearing`)
return false
}
entry.terminal.show(preserveFocus)
this.log(`showExisting: revealed terminal for session ${sessionId}`)
return true
}
/**
* Check if a session has an active terminal.
*/
hasTerminal(sessionId: string): boolean {
const entry = this.terminals.get(sessionId)
return entry !== undefined && entry.terminal.exitStatus === undefined
}
dispose(): void {
vscode.commands.executeCommand("setContext", "kilo-code.agentTerminalFocus", false)
for (const entry of this.terminals.values()) entry.terminal.dispose()
this.terminals.clear()
for (const d of this.disposables) d.dispose()
}
private isManaged(terminal: vscode.Terminal): boolean {
for (const entry of this.terminals.values()) {
if (entry.terminal === terminal) return true
}
return false
}
private updateContextKey(): void {
const active = vscode.window.activeTerminal
const managed = active ? this.isManaged(active) : false
vscode.commands.executeCommand("setContext", "kilo-code.agentTerminalFocus", managed)
}
private showOrCreate(sessionId: string, cwd: string, name: string): void {
let entry = this.terminals.get(sessionId)
// Clean up exited terminals
if (entry && entry.terminal.exitStatus !== undefined) {
this.terminals.delete(sessionId)
entry = undefined
}
// Recreate if CWD changed
if (entry && entry.cwd !== cwd) {
entry.terminal.dispose()
this.terminals.delete(sessionId)
entry = undefined
this.log(`showTerminal: cwd changed for session ${sessionId}, recreating`)
}
if (!entry) {
const terminal = vscode.window.createTerminal({
cwd,
name,
iconPath: new vscode.ThemeIcon("terminal"),
})
entry = { terminal, cwd }
this.terminals.set(sessionId, entry)
this.log(`showTerminal: created terminal for session ${sessionId} (cwd=${cwd})`)
}
entry.terminal.show(false)
this.updateContextKey()
}
}
+36
View File
@@ -6,6 +6,8 @@ import { KiloConnectionService } from "./services/cli-backend"
import { registerAutocompleteProvider } from "./services/autocomplete"
import { BrowserAutomationService } from "./services/browser-automation"
import { TelemetryProxy } from "./services/telemetry"
import { registerCommitMessageService } from "./services/commit-message"
import { registerCodeActions, registerTerminalActions, KiloCodeActionProvider } from "./services/code-actions"
export function activate(context: vscode.ExtensionContext) {
console.log("Kilo Code extension is now active")
@@ -80,11 +82,45 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("kilo-code.new.agentManager.nextTab", () => {
agentManagerProvider.postMessage({ type: "action", action: "tabNext" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.showTerminal", () => {
agentManagerProvider.showTerminalForCurrentSession()
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.focusPanel", () => {
agentManagerProvider.focusPanel()
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.newTab", () => {
agentManagerProvider.postMessage({ type: "action", action: "newTab" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.closeTab", () => {
agentManagerProvider.postMessage({ type: "action", action: "closeTab" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.newWorktree", () => {
agentManagerProvider.postMessage({ type: "action", action: "newWorktree" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.closeWorktree", () => {
agentManagerProvider.postMessage({ type: "action", action: "closeWorktree" })
}),
)
// Register autocomplete provider
registerAutocompleteProvider(context, connectionService)
// Register commit message generation
registerCommitMessageService(context, connectionService)
// Register code actions (editor context menus, terminal context menus, keyboard shortcuts)
registerCodeActions(context, provider)
registerTerminalActions(context, provider)
// Register CodeActionProvider (lightbulb quick fixes)
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
{ scheme: "file" },
new KiloCodeActionProvider(),
KiloCodeActionProvider.metadata,
),
)
// Dispose services when extension deactivates (kills the server)
context.subscriptions.push({
dispose: () => {
@@ -24,8 +24,8 @@ export class HttpClient {
constructor(config: ServerConfig) {
this.baseUrl = config.baseUrl
// Auth header format: Basic base64("opencode:password")
// NOTE: The CLI server expects a non-empty username ("opencode"). Using an empty username
// Auth header format: Basic base64("kilo:password")
// NOTE: The CLI server expects a non-empty username ("kilo"). Using an empty username
// (":password") results in 401 for both REST and SSE endpoints.
this.authHeader = `Basic ${Buffer.from(`${this.authUsername}:${config.password}`).toString("base64")}`
@@ -478,6 +478,22 @@ export class HttpClient {
return this.request<string[]>("GET", `/find/file?${params.toString()}`, undefined, { directory })
}
// ============================================
// Commit Message Methods
// ============================================
/**
* Generate a commit message for the current diff in the given directory.
*/
async generateCommitMessage(path: string, selectedFiles?: string[], previousMessage?: string): Promise<string> {
const result = await this.request<{ message: string }>("POST", "/commit-message", {
path,
selectedFiles,
previousMessage,
})
return result.message
}
// ============================================
// MCP Methods
// ============================================
@@ -0,0 +1,42 @@
import * as vscode from "vscode"
export class KiloCodeActionProvider implements vscode.CodeActionProvider {
static readonly metadata: vscode.CodeActionProviderMetadata = {
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix, vscode.CodeActionKind.RefactorRewrite],
}
provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range | vscode.Selection,
context: vscode.CodeActionContext,
): vscode.CodeAction[] {
if (range.isEmpty) return []
const actions: vscode.CodeAction[] = []
const add = new vscode.CodeAction("Add to Kilo Code", vscode.CodeActionKind.RefactorRewrite)
add.command = { command: "kilo-code.new.addToContext", title: "Add to Kilo Code" }
actions.push(add)
const hasDiagnostics = context.diagnostics.length > 0
if (hasDiagnostics) {
const fix = new vscode.CodeAction("Fix with Kilo Code", vscode.CodeActionKind.QuickFix)
fix.command = { command: "kilo-code.new.fixCode", title: "Fix with Kilo Code" }
fix.isPreferred = true
actions.push(fix)
}
if (!hasDiagnostics) {
const explain = new vscode.CodeAction("Explain with Kilo Code", vscode.CodeActionKind.RefactorRewrite)
explain.command = { command: "kilo-code.new.explainCode", title: "Explain with Kilo Code" }
actions.push(explain)
const improve = new vscode.CodeAction("Improve with Kilo Code", vscode.CodeActionKind.RefactorRewrite)
improve.command = { command: "kilo-code.new.improveCode", title: "Improve with Kilo Code" }
actions.push(improve)
}
return actions
}
}
@@ -0,0 +1,24 @@
import * as vscode from "vscode"
export interface EditorContext {
filePath: string
selectedText: string
startLine: number
endLine: number
diagnostics: vscode.Diagnostic[]
}
export function getEditorContext(): EditorContext | undefined {
const editor = vscode.window.activeTextEditor
if (!editor) return undefined
const selection = editor.selection
if (selection.isEmpty) return undefined
const doc = editor.document
return {
filePath: vscode.workspace.asRelativePath(doc.uri),
selectedText: doc.getText(selection),
startLine: selection.start.line + 1,
endLine: selection.end.line + 1,
diagnostics: vscode.languages.getDiagnostics(doc.uri).filter((d) => d.range.intersection(selection) !== undefined),
}
}
@@ -0,0 +1,3 @@
export { registerCodeActions } from "./register-code-actions"
export { registerTerminalActions } from "./register-terminal-actions"
export { KiloCodeActionProvider } from "./code-action-provider"
@@ -0,0 +1,65 @@
import * as vscode from "vscode"
import type { KiloProvider } from "../../KiloProvider"
import { getEditorContext } from "./editor-utils"
import { createPrompt } from "./support-prompt"
export function registerCodeActions(context: vscode.ExtensionContext, provider: KiloProvider): void {
context.subscriptions.push(
vscode.commands.registerCommand("kilo-code.new.explainCode", () => {
const ctx = getEditorContext()
if (!ctx) return
const prompt = createPrompt("EXPLAIN", {
filePath: ctx.filePath,
startLine: String(ctx.startLine),
endLine: String(ctx.endLine),
selectedText: ctx.selectedText,
userInput: "",
})
provider.postMessage({ type: "triggerTask", text: prompt })
}),
vscode.commands.registerCommand("kilo-code.new.fixCode", () => {
const ctx = getEditorContext()
if (!ctx) return
const prompt = createPrompt("FIX", {
filePath: ctx.filePath,
startLine: String(ctx.startLine),
endLine: String(ctx.endLine),
selectedText: ctx.selectedText,
diagnostics: ctx.diagnostics,
userInput: "",
})
provider.postMessage({ type: "triggerTask", text: prompt })
}),
vscode.commands.registerCommand("kilo-code.new.improveCode", () => {
const ctx = getEditorContext()
if (!ctx) return
const prompt = createPrompt("IMPROVE", {
filePath: ctx.filePath,
startLine: String(ctx.startLine),
endLine: String(ctx.endLine),
selectedText: ctx.selectedText,
userInput: "",
})
provider.postMessage({ type: "triggerTask", text: prompt })
}),
vscode.commands.registerCommand("kilo-code.new.addToContext", () => {
const ctx = getEditorContext()
if (!ctx) return
const prompt = createPrompt("ADD_TO_CONTEXT", {
filePath: ctx.filePath,
startLine: String(ctx.startLine),
endLine: String(ctx.endLine),
selectedText: ctx.selectedText,
})
provider.postMessage({ type: "setChatBoxMessage", text: prompt })
provider.postMessage({ type: "action", action: "focusInput" })
}),
vscode.commands.registerCommand("kilo-code.new.focusChatInput", () => {
provider.postMessage({ type: "action", action: "focusInput" })
}),
)
}
@@ -0,0 +1,57 @@
import * as vscode from "vscode"
import type { KiloProvider } from "../../KiloProvider"
import { createPrompt } from "./support-prompt"
function getTerminalSelection(): string {
const terminal = vscode.window.activeTerminal
if (!terminal) return ""
// VS Code terminal API doesn't expose buffer contents directly.
// Terminal selection is available via clipboard in some cases.
// For now, this is a placeholder — full implementation requires
// VS Code shell integration API (terminal.shellIntegration).
return ""
}
export function registerTerminalActions(context: vscode.ExtensionContext, provider: KiloProvider): void {
context.subscriptions.push(
vscode.commands.registerCommand("kilo-code.new.terminalAddToContext", () => {
const content = getTerminalSelection()
if (!content) {
vscode.window.showInformationMessage("No terminal content available. Select text in the terminal first.")
return
}
const prompt = createPrompt("TERMINAL_ADD_TO_CONTEXT", {
terminalContent: content,
userInput: "",
})
provider.postMessage({ type: "setChatBoxMessage", text: prompt })
provider.postMessage({ type: "action", action: "focusInput" })
}),
vscode.commands.registerCommand("kilo-code.new.terminalFixCommand", () => {
const content = getTerminalSelection()
if (!content) {
vscode.window.showInformationMessage("No terminal content available. Select text in the terminal first.")
return
}
const prompt = createPrompt("TERMINAL_FIX", {
terminalContent: content,
userInput: "",
})
provider.postMessage({ type: "triggerTask", text: prompt })
}),
vscode.commands.registerCommand("kilo-code.new.terminalExplainCommand", () => {
const content = getTerminalSelection()
if (!content) {
vscode.window.showInformationMessage("No terminal content available. Select text in the terminal first.")
return
}
const prompt = createPrompt("TERMINAL_EXPLAIN", {
terminalContent: content,
userInput: "",
})
provider.postMessage({ type: "triggerTask", text: prompt })
}),
)
}
@@ -0,0 +1,107 @@
type Params = Record<string, string | any[]>
function diagnosticText(diagnostics?: any[]) {
if (!diagnostics?.length) return ""
return `\nCurrent problems detected:\n${diagnostics
.map((d) => `- [${d.source || "Error"}] ${d.message}${d.code ? ` (${d.code})` : ""}`)
.join("\n")}`
}
function fill(template: string, params: Params): string {
return template.replace(/\${(.*?)}/g, (_, key) => {
if (key === "diagnosticText") return diagnosticText(params["diagnostics"] as any[])
if (key in params) return String(params[key] ?? "")
return ""
})
}
type PromptType =
| "EXPLAIN"
| "FIX"
| "IMPROVE"
| "ADD_TO_CONTEXT"
| "TERMINAL_ADD_TO_CONTEXT"
| "TERMINAL_FIX"
| "TERMINAL_EXPLAIN"
const templates: Record<PromptType, string> = {
EXPLAIN: `Explain the following code from file path \${filePath}:\${startLine}-\${endLine}
\${userInput}
\`\`\`
\${selectedText}
\`\`\`
Please provide a clear and concise explanation of what this code does, including:
1. The purpose and functionality
2. Key components and their interactions
3. Important patterns or techniques used`,
FIX: `Fix any issues in the following code from file path \${filePath}:\${startLine}-\${endLine}
\${diagnosticText}
\${userInput}
\`\`\`
\${selectedText}
\`\`\`
Please:
1. Address all detected problems listed above (if any)
2. Identify any other potential bugs or issues
3. Provide corrected code
4. Explain what was fixed and why`,
IMPROVE: `Improve the following code from file path \${filePath}:\${startLine}-\${endLine}
\${userInput}
\`\`\`
\${selectedText}
\`\`\`
Please suggest improvements for:
1. Code readability and maintainability
2. Performance optimization
3. Best practices and patterns
4. Error handling and edge cases
Provide the improved code along with explanations for each enhancement.`,
ADD_TO_CONTEXT: `\${filePath}:\${startLine}-\${endLine}
\`\`\`
\${selectedText}
\`\`\``,
TERMINAL_ADD_TO_CONTEXT: `\${userInput}
Terminal output:
\`\`\`
\${terminalContent}
\`\`\``,
TERMINAL_FIX: `\${userInput}
Fix this terminal command:
\`\`\`
\${terminalContent}
\`\`\`
Please:
1. Identify any issues in the command
2. Provide the corrected command
3. Explain what was fixed and why`,
TERMINAL_EXPLAIN: `\${userInput}
Explain this terminal command:
\`\`\`
\${terminalContent}
\`\`\`
Please provide:
1. What the command does
2. Explanation of each part/flag
3. Expected output and behavior`,
}
export function createPrompt(type: PromptType, params: Params): string {
return fill(templates[type], params)
}
export type { PromptType }
@@ -0,0 +1,207 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
// Mock vscode following the pattern from AutocompleteServiceManager.spec.ts
vi.mock("vscode", () => {
const disposable = { dispose: vi.fn() }
return {
commands: {
registerCommand: vi.fn((_command: string, _callback: (...args: any[]) => any) => disposable),
},
window: {
showErrorMessage: vi.fn(),
withProgress: vi.fn(),
},
workspace: {
workspaceFolders: [
{
uri: { fsPath: "/test/workspace" },
},
],
},
extensions: {
getExtension: vi.fn(),
},
ProgressLocation: {
SourceControl: 1,
},
Uri: {
parse: (s: string) => ({ fsPath: s }),
},
}
})
import * as vscode from "vscode"
import { registerCommitMessageService } from "../index"
import type { KiloConnectionService } from "../../cli-backend/connection-service"
describe("commit-message service", () => {
let mockContext: vscode.ExtensionContext
let mockConnectionService: KiloConnectionService
let mockHttpClient: { generateCommitMessage: ReturnType<typeof vi.fn> }
beforeEach(() => {
vi.clearAllMocks()
mockContext = {
subscriptions: [],
} as any
mockHttpClient = {
generateCommitMessage: vi.fn().mockResolvedValue("feat: add new feature"),
}
mockConnectionService = {
getHttpClient: vi.fn().mockReturnValue(mockHttpClient),
} as any
})
describe("registerCommitMessageService", () => {
it("returns an array of disposables", () => {
const disposables = registerCommitMessageService(mockContext, mockConnectionService)
expect(Array.isArray(disposables)).toBe(true)
expect(disposables.length).toBeGreaterThan(0)
})
it("registers the kilo-code.new.generateCommitMessage command", () => {
registerCommitMessageService(mockContext, mockConnectionService)
expect(vscode.commands.registerCommand).toHaveBeenCalledWith(
"kilo-code.new.generateCommitMessage",
expect.any(Function),
)
})
it("pushes the command disposable to context.subscriptions", () => {
registerCommitMessageService(mockContext, mockConnectionService)
expect(mockContext.subscriptions.length).toBe(1)
})
})
describe("command execution", () => {
let commandCallback: (...args: any[]) => Promise<void>
beforeEach(() => {
registerCommitMessageService(mockContext, mockConnectionService)
// Extract the registered command callback
const registerCall = vi.mocked(vscode.commands.registerCommand).mock.calls[0]!
commandCallback = registerCall[1] as (...args: any[]) => Promise<void>
})
it("shows error when git extension is not found", async () => {
vi.mocked(vscode.extensions.getExtension).mockReturnValue(undefined)
await commandCallback()
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Git extension not found")
})
it("shows error when no git repository is found", async () => {
vi.mocked(vscode.extensions.getExtension).mockReturnValue({
isActive: true,
activate: vi.fn().mockResolvedValue(undefined),
exports: {
getAPI: () => ({ repositories: [] }),
},
} as any)
await commandCallback()
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("No Git repository found")
})
it("shows error when backend is not connected", async () => {
vi.mocked(vscode.extensions.getExtension).mockReturnValue({
isActive: true,
activate: vi.fn().mockResolvedValue(undefined),
exports: {
getAPI: () => ({
repositories: [{ inputBox: { value: "" }, rootUri: { fsPath: "/repo" } }],
}),
},
} as any)
vi.mocked(mockConnectionService.getHttpClient as any).mockImplementation(() => {
throw new Error("Not connected")
})
await commandCallback()
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
"Kilo backend is not connected. Please wait for the connection to establish.",
)
})
it("calls generateCommitMessage on the HTTP client with repository root path", async () => {
const mockInputBox = { value: "" }
vi.mocked(vscode.extensions.getExtension).mockReturnValue({
isActive: true,
activate: vi.fn().mockResolvedValue(undefined),
exports: {
getAPI: () => ({
repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/repo" } }],
}),
},
} as any)
// Make withProgress execute its callback
vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => {
await task({} as any, {} as any)
})
await commandCallback()
expect(mockHttpClient.generateCommitMessage).toHaveBeenCalledWith("/repo", undefined, undefined)
})
it("sets the generated message on the repository inputBox", async () => {
const mockInputBox = { value: "" }
vi.mocked(vscode.extensions.getExtension).mockReturnValue({
isActive: true,
activate: vi.fn().mockResolvedValue(undefined),
exports: {
getAPI: () => ({
repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/repo" } }],
}),
},
} as any)
vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => {
await task({} as any, {} as any)
})
await commandCallback()
expect(mockInputBox.value).toBe("feat: add new feature")
})
it("shows progress in SourceControl location", async () => {
const mockInputBox = { value: "" }
vi.mocked(vscode.extensions.getExtension).mockReturnValue({
isActive: true,
activate: vi.fn().mockResolvedValue(undefined),
exports: {
getAPI: () => ({
repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/repo" } }],
}),
},
} as any)
vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => {
await task({} as any, {} as any)
})
await commandCallback()
expect(vscode.window.withProgress).toHaveBeenCalledWith(
expect.objectContaining({
location: vscode.ProgressLocation.SourceControl,
title: "Generating commit message...",
}),
expect.any(Function),
)
})
})
})
@@ -0,0 +1,79 @@
import * as vscode from "vscode"
import type { KiloConnectionService } from "../cli-backend/connection-service"
import type { HttpClient } from "../cli-backend/http-client"
let lastGeneratedMessage: string | undefined
let lastWorkspacePath: string | undefined
interface GitRepository {
inputBox: { value: string }
rootUri: vscode.Uri
}
interface GitAPI {
repositories: GitRepository[]
}
interface GitExtensionExports {
getAPI(version: number): GitAPI
}
export function registerCommitMessageService(
context: vscode.ExtensionContext,
connectionService: KiloConnectionService,
): vscode.Disposable[] {
const command = vscode.commands.registerCommand("kilo-code.new.generateCommitMessage", async () => {
const extension = vscode.extensions.getExtension<GitExtensionExports>("vscode.git")
if (!extension) {
vscode.window.showErrorMessage("Git extension not found")
return
}
if (!extension.isActive) {
await extension.activate()
}
const git = extension.exports?.getAPI(1)
const repository = git?.repositories[0]
if (!repository) {
vscode.window.showErrorMessage("No Git repository found")
return
}
let client: HttpClient | undefined
try {
client = connectionService.getHttpClient()
} catch {
vscode.window.showErrorMessage("Kilo backend is not connected. Please wait for the connection to establish.")
return
}
if (!client) {
vscode.window.showErrorMessage("Kilo backend is not connected. Please wait for the connection to establish.")
return
}
const path = repository.rootUri.fsPath
const previousMessage = lastWorkspacePath === path ? lastGeneratedMessage : undefined
await vscode.window
.withProgress(
{ location: vscode.ProgressLocation.SourceControl, title: "Generating commit message..." },
async () => {
const message = await client.generateCommitMessage(path, undefined, previousMessage)
repository.inputBox.value = message
lastGeneratedMessage = message
lastWorkspacePath = path
console.log("[Kilo New] Commit message generated successfully")
},
)
.then(undefined, (error: unknown) => {
const msg = error instanceof Error ? error.message : String(error)
console.error("[Kilo New] Failed to generate commit message:", msg)
vscode.window.showErrorMessage(`Failed to generate commit message: ${msg}`)
})
})
context.subscriptions.push(command)
return [command]
}
@@ -3,16 +3,19 @@
*
* The agent manager runs in the same webview context as other UI.
* All its CSS classes must be prefixed with "am-" to avoid conflicts.
* These tests also verify consistency between CSS definitions and TSX usage.
* These tests also verify consistency between CSS definitions and TSX usage,
* and that the provider sends correct message types for each action.
*/
import { describe, it, expect } from "bun:test"
import fs from "node:fs"
import path from "node:path"
import { Project, SyntaxKind } from "ts-morph"
const ROOT = path.resolve(import.meta.dir, "../..")
const CSS_FILE = path.join(ROOT, "webview-ui/agent-manager/agent-manager.css")
const TSX_FILE = path.join(ROOT, "webview-ui/agent-manager/AgentManagerApp.tsx")
const PROVIDER_FILE = path.join(ROOT, "src/agent-manager/AgentManagerProvider.ts")
describe("Agent Manager CSS Prefix", () => {
it("all class selectors should use am- prefix", () => {
@@ -79,3 +82,29 @@ describe("Agent Manager CSS/TSX Consistency", () => {
expect(unused, `Classes defined in CSS but not used in TSX: ${unused.join(", ")}`).toEqual([])
})
})
describe("Agent Manager Provider Messages", () => {
function getMethodBody(name: string): string {
const project = new Project({ compilerOptions: { allowJs: true } })
const source = project.addSourceFileAtPath(PROVIDER_FILE)
const cls = source.getFirstDescendantByKind(SyntaxKind.ClassDeclaration)
const method = cls?.getMethod(name)
expect(method, `method ${name} not found in AgentManagerProvider`).toBeTruthy()
return method!.getText()
}
/**
* Regression: onAddSessionToWorktree must NOT send agentManager.worktreeSetup
* because that triggers a full-screen overlay with a spinner. Adding a session
* to an existing worktree should use agentManager.sessionAdded instead.
*/
it("onAddSessionToWorktree should not send worktreeSetup messages", () => {
const body = getMethodBody("onAddSessionToWorktree")
expect(body).not.toContain("agentManager.worktreeSetup")
})
it("onAddSessionToWorktree should send sessionAdded message", () => {
const body = getMethodBody("onAddSessionToWorktree")
expect(body).toContain("agentManager.sessionAdded")
})
})
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test"
import { resolveNavigation, validateLocalSession } from "../../webview-ui/agent-manager/navigate"
import { resolveNavigation, validateLocalSession, LOCAL } from "../../webview-ui/agent-manager/navigate"
const ids = ["a", "b", "c", "d"]
@@ -24,7 +24,7 @@ describe("resolveNavigation", () => {
describe("from first session", () => {
it("up → local", () => {
expect(resolveNavigation("up", "a", ids)).toEqual({ action: "local" })
expect(resolveNavigation("up", "a", ids)).toEqual({ action: LOCAL })
})
it("down → selects second session", () => {
@@ -68,7 +68,7 @@ describe("resolveNavigation", () => {
})
it("up from only session → local", () => {
expect(resolveNavigation("up", "x", ["x"])).toEqual({ action: "local" })
expect(resolveNavigation("up", "x", ["x"])).toEqual({ action: LOCAL })
})
it("down from only session → none", () => {
@@ -95,20 +95,20 @@ describe("resolveNavigation", () => {
expect(trail).toEqual(["s1", "s2", "s3"])
// Navigate back up through all sessions to local
const upTrail: (string | "local")[] = []
const upTrail: (string | typeof LOCAL)[] = []
for (let i = 0; i < 4; i++) {
const result = resolveNavigation("up", current, sessions)
if (result.action === "select") {
current = result.id
upTrail.push(current)
} else if (result.action === "local") {
} else if (result.action === LOCAL) {
current = undefined
upTrail.push("local")
upTrail.push(LOCAL)
} else {
break
}
}
expect(upTrail).toEqual(["s2", "s1", "local"])
expect(upTrail).toEqual(["s2", "s1", LOCAL])
})
})
})
@@ -0,0 +1,36 @@
import { describe, it, expect } from "bun:test"
import { nextSelectionAfterDelete, LOCAL } from "../../webview-ui/agent-manager/navigate"
describe("nextSelectionAfterDelete", () => {
it("selects the worktree below when deleting from the middle", () => {
expect(nextSelectionAfterDelete("b", ["a", "b", "c"])).toBe("c")
})
it("selects the worktree above when deleting the last item", () => {
expect(nextSelectionAfterDelete("c", ["a", "b", "c"])).toBe("b")
})
it("selects the worktree below when deleting the first item", () => {
expect(nextSelectionAfterDelete("a", ["a", "b", "c"])).toBe("b")
})
it("falls back to LOCAL when deleting the only worktree", () => {
expect(nextSelectionAfterDelete("a", ["a"])).toBe(LOCAL)
})
it("falls back to LOCAL when ID is not found", () => {
expect(nextSelectionAfterDelete("x", ["a", "b"])).toBe(LOCAL)
})
it("falls back to LOCAL when list is empty", () => {
expect(nextSelectionAfterDelete("a", [])).toBe(LOCAL)
})
it("handles two-item list deleting first", () => {
expect(nextSelectionAfterDelete("a", ["a", "b"])).toBe("b")
})
it("handles two-item list deleting second", () => {
expect(nextSelectionAfterDelete("b", ["a", "b"])).toBe("a")
})
})
@@ -11,7 +11,8 @@ import type {
SessionInfo,
} from "../src/types/messages"
import { ThemeProvider } from "@kilocode/kilo-ui/theme"
import { DialogProvider } from "@kilocode/kilo-ui/context/dialog"
import { DialogProvider, useDialog } from "@kilocode/kilo-ui/context/dialog"
import { Dialog } from "@kilocode/kilo-ui/dialog"
import { MarkedProvider } from "@kilocode/kilo-ui/context/marked"
import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code"
import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff"
@@ -32,7 +33,7 @@ import { WorktreeModeProvider } from "../src/context/worktree-mode"
import { ChatView } from "../src/components/chat"
import { LanguageBridge, DataBridge } from "../src/App"
import { formatRelativeDate } from "../src/utils/date"
import { validateLocalSession } from "./navigate"
import { validateLocalSession, nextSelectionAfterDelete, LOCAL } from "./navigate"
import "./agent-manager.css"
interface SetupState {
@@ -42,18 +43,23 @@ interface SetupState {
error?: boolean
}
/** Sidebar selection: "local" for workspace, worktree ID for a worktree, or null for an unassigned session. */
type SidebarSelection = "local" | string | null
/** Sidebar selection: LOCAL for workspace, worktree ID for a worktree, or null for an unassigned session. */
type SidebarSelection = typeof LOCAL | string | null
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
const modKey = isMac ? "\u2318" : "Ctrl+"
const AgentManagerContent: Component = () => {
const session = useSession()
const vscode = useVSCode()
const dialog = useDialog()
const [setup, setSetup] = createSignal<SetupState>({ active: false, message: "" })
const [worktrees, setWorktrees] = createSignal<WorktreeState[]>([])
const [managedSessions, setManagedSessions] = createSignal<ManagedSessionState[]>([])
const [selection, setSelection] = createSignal<SidebarSelection>("local")
const [selection, setSelection] = createSignal<SidebarSelection>(LOCAL)
const [repoBranch, setRepoBranch] = createSignal<string | undefined>()
const [deletingWorktrees, setDeletingWorktrees] = createSignal<Set<string>>(new Set())
// Recover persisted local session IDs from webview state
const persisted = vscode.getState<{ localSessionIDs?: string[] }>()
@@ -129,7 +135,7 @@ const AgentManagerContent: Component = () => {
// Sessions for the currently selected worktree (tab bar), sorted by creation date
const activeWorktreeSessions = createMemo((): SessionInfo[] => {
const sel = selection()
if (!sel || sel === "local") return []
if (!sel || sel === LOCAL) return []
const managed = managedSessions().filter((ms) => ms.worktreeId === sel)
const ids = new Set(managed.map((ms) => ms.id))
return session
@@ -141,7 +147,7 @@ const AgentManagerContent: Component = () => {
// Active tab sessions: local sessions when on "local", worktree sessions otherwise
const activeTabs = createMemo((): SessionInfo[] => {
const sel = selection()
if (sel === "local") return localSessions()
if (sel === LOCAL) return localSessions()
if (sel) return activeWorktreeSessions()
return []
})
@@ -149,7 +155,7 @@ const AgentManagerContent: Component = () => {
// Whether the selected context has zero sessions
const contextEmpty = createMemo(() => {
const sel = selection()
if (sel === "local") return localSessionIDs().length === 0
if (sel === LOCAL) return localSessionIDs().length === 0
if (sel) return activeWorktreeSessions().length === 0
return false
})
@@ -171,8 +177,8 @@ const AgentManagerContent: Component = () => {
// Navigate sidebar items with arrow keys
const navigate = (direction: "up" | "down") => {
const flat: { type: "local" | "wt" | "session"; id: string }[] = [
{ type: "local", id: "local" },
const flat: { type: typeof LOCAL | "wt" | "session"; id: string }[] = [
{ type: LOCAL, id: LOCAL },
...worktrees().map((wt) => ({ type: "wt" as const, id: wt.id })),
...unassignedSessions().map((s) => ({ type: "session" as const, id: s.id })),
]
@@ -184,7 +190,7 @@ const AgentManagerContent: Component = () => {
if (next < 0 || next >= flat.length) return
const item = flat[next]!
if (item.type === "local") {
if (item.type === LOCAL) {
selectLocal()
} else if (item.type === "wt") {
selectWorktree(item.id)
@@ -217,7 +223,7 @@ const AgentManagerContent: Component = () => {
}
const selectLocal = () => {
setSelection("local")
setSelection(LOCAL)
vscode.postMessage({ type: "agentManager.requestRepoInfo" })
const locals = localSessions()
const first = locals[0]
@@ -253,17 +259,32 @@ const AgentManagerContent: Component = () => {
else if (msg.action === "sessionNext") navigate("down")
else if (msg.action === "tabPrevious") navigateTab("left")
else if (msg.action === "tabNext") navigateTab("right")
else if (msg.action === "showTerminal") {
const id = session.currentSessionID()
if (id) vscode.postMessage({ type: "agentManager.showTerminal", sessionId: id })
} else if (msg.action === "newTab") handleNewTabForCurrentSelection()
else if (msg.action === "closeTab") closeActiveTab()
else if (msg.action === "newWorktree") handleNewWorktreeOrPromote()
else if (msg.action === "closeWorktree") closeSelectedWorktree()
}
window.addEventListener("message", handler)
// Prevent Cmd+Up/Down/Left/Right from triggering native scroll
const preventScroll = (e: KeyboardEvent) => {
// Prevent Cmd+Arrow/T/W/N from triggering native browser actions
const preventDefaults = (e: KeyboardEvent) => {
if (!(e.metaKey || e.ctrlKey)) return
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
e.preventDefault()
}
// Prevent browser defaults for our shortcuts (new tab, close tab, new window)
if (["t", "w", "n"].includes(e.key.toLowerCase()) && !e.shiftKey) {
e.preventDefault()
}
// Prevent defaults for shift variants (close worktree)
if (e.key.toLowerCase() === "w" && e.shiftKey) {
e.preventDefault()
}
}
window.addEventListener("keydown", preventScroll)
window.addEventListener("keydown", preventDefaults)
// When the panel regains focus (e.g. returning from terminal), focus the prompt
const onWindowFocus = () => window.dispatchEvent(new Event("focusPrompt"))
@@ -272,7 +293,7 @@ const AgentManagerContent: Component = () => {
// When a session is created while on local, replace the current pending tab with the real session.
// Guard against duplicate sessionCreated events (HTTP response + SSE can both fire).
const unsubCreate = vscode.onMessage((msg) => {
if (msg.type === "sessionCreated" && selection() === "local") {
if (msg.type === "sessionCreated" && selection() === LOCAL) {
const created = msg as { type: string; session: { id: string } }
if (localSessionIDs().includes(created.session.id)) return
const pending = activePendingId()
@@ -299,12 +320,20 @@ const AgentManagerContent: Component = () => {
globalThis.setTimeout(() => setSetup({ active: false, message: "" }), error ? 3000 : 500)
if (!error && ev.sessionId) {
session.selectSession(ev.sessionId)
// Auto-switch sidebar to the worktree containing this session
const ms = managedSessions().find((s) => s.id === ev.sessionId)
if (ms?.worktreeId) setSelection(ms.worktreeId)
}
} else {
setSetup({ active: true, message: ev.message, branch: ev.branch })
}
}
if (msg.type === "agentManager.sessionAdded") {
const ev = msg as { type: string; sessionId: string; worktreeId: string }
session.selectSession(ev.sessionId)
}
if (msg.type === "agentManager.state") {
const state = msg as AgentManagerStateMessage
setWorktrees(state.worktrees)
@@ -314,12 +343,18 @@ const AgentManagerContent: Component = () => {
const ms = state.sessions.find((s) => s.id === current)
if (ms?.worktreeId) setSelection(ms.worktreeId)
}
// Clear deleting state for worktrees that have been removed
const ids = new Set(state.worktrees.map((wt) => wt.id))
setDeletingWorktrees((prev) => {
const next = new Set([...prev].filter((id) => ids.has(id)))
return next.size === prev.size ? prev : next
})
}
})
onCleanup(() => {
window.removeEventListener("message", handler)
window.removeEventListener("keydown", preventScroll)
window.removeEventListener("keydown", preventDefaults)
window.removeEventListener("focus", onWindowFocus)
unsubCreate()
unsub()
@@ -339,10 +374,48 @@ const AgentManagerContent: Component = () => {
vscode.postMessage({ type: "agentManager.createWorktree" })
}
const confirmDeleteWorktree = (worktreeId: string) => {
const wt = worktrees().find((w) => w.id === worktreeId)
if (!wt) return
const doDelete = () => {
setDeletingWorktrees((prev) => new Set([...prev, wt.id]))
vscode.postMessage({ type: "agentManager.deleteWorktree", worktreeId: wt.id })
if (selection() === wt.id) {
const next = nextSelectionAfterDelete(
wt.id,
worktrees().map((w) => w.id),
)
if (next === LOCAL) selectLocal()
else selectWorktree(next)
}
dialog.close()
}
dialog.show(() => (
<Dialog title="Delete Worktree" fit>
<div class="am-confirm">
<div class="am-confirm-message">
<Icon name="trash" size="small" />
<span>
Delete worktree <code class="am-confirm-branch">{wt.branch}</code>? This removes the worktree from disk
and dissociates all sessions.
</span>
</div>
<div class="am-confirm-actions">
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
Cancel
</Button>
<Button variant="primary" size="large" class="am-confirm-delete" onClick={doDelete} autofocus>
Delete
</Button>
</div>
</div>
</Dialog>
))
}
const handleDeleteWorktree = (worktreeId: string, e: MouseEvent) => {
e.stopPropagation()
vscode.postMessage({ type: "agentManager.deleteWorktree", worktreeId })
if (selection() === worktreeId) selectLocal()
confirmDeleteWorktree(worktreeId)
}
const handlePromote = (sessionId: string, e: MouseEvent) => {
@@ -352,7 +425,7 @@ const AgentManagerContent: Component = () => {
const handleAddSession = () => {
const sel = selection()
if (sel === "local") {
if (sel === LOCAL) {
addPendingTab()
} else if (sel) {
vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel })
@@ -394,12 +467,61 @@ const AgentManagerContent: Component = () => {
}
}
// Close the currently active tab via keyboard shortcut.
// If no tabs remain, fall through to close the selected worktree.
const closeActiveTab = () => {
const tabs = activeTabs()
if (tabs.length === 0) {
closeSelectedWorktree()
return
}
const current = session.currentSessionID()
const pending = activePendingId()
const target = current
? tabs.find((s) => s.id === current)
: pending
? tabs.find((s) => s.id === pending)
: undefined
if (!target) return
const synthetic = new MouseEvent("click")
handleCloseTab(target.id, synthetic)
}
// Cmd+T: add a new tab strictly to the current selection (no side effects)
const handleNewTabForCurrentSelection = () => {
const sel = selection()
if (sel === LOCAL) {
addPendingTab()
} else if (sel) {
// Pass the captured worktree ID directly to avoid race conditions
vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel })
}
}
// Cmd+N: if an unassigned session is selected, promote it; otherwise create a new worktree
const handleNewWorktreeOrPromote = () => {
const sel = selection()
const sid = session.currentSessionID()
if (sel === null && sid && !worktreeSessionIds().has(sid)) {
vscode.postMessage({ type: "agentManager.promoteSession", sessionId: sid })
return
}
handleCreateWorktree()
}
// Close the currently selected worktree with a confirmation dialog
const closeSelectedWorktree = () => {
const sel = selection()
if (!sel || sel === LOCAL) return
confirmDeleteWorktree(sel)
}
return (
<div class="am-layout">
<div class="am-sidebar">
{/* Local workspace item */}
<button
class={`am-local-item ${selection() === "local" ? "am-local-item-active" : ""}`}
class={`am-local-item ${selection() === LOCAL ? "am-local-item-active" : ""}`}
data-sidebar-id="local"
onClick={() => selectLocal()}
>
@@ -434,14 +556,16 @@ const AgentManagerContent: Component = () => {
<span class="am-worktree-branch" title={wt.branch}>
{worktreeLabel(wt)}
</span>
<IconButton
icon="close-small"
size="small"
variant="ghost"
label="Close worktree"
class="am-worktree-close"
onClick={(e: MouseEvent) => handleDeleteWorktree(wt.id, e)}
/>
<Show when={!deletingWorktrees().has(wt.id)} fallback={<Spinner class="am-worktree-spinner" />}>
<IconButton
icon="close-small"
size="small"
variant="ghost"
label="Close worktree"
class="am-worktree-close"
onClick={(e: MouseEvent) => handleDeleteWorktree(wt.id, e)}
/>
</Show>
</div>
)}
</For>
@@ -533,10 +657,24 @@ const AgentManagerContent: Component = () => {
icon="plus"
size="small"
variant="ghost"
label="New session"
label={`New session (${modKey}T)`}
class="am-tab-add"
onClick={handleAddSession}
/>
<div class="am-tab-terminal">
<Tooltip value="Open Terminal" placement="bottom">
<IconButton
icon="console"
size="small"
variant="ghost"
label="Open Terminal"
onClick={() => {
const id = session.currentSessionID()
if (id) vscode.postMessage({ type: "agentManager.showTerminal", sessionId: id })
}}
/>
</Tooltip>
</div>
</div>
</Show>
@@ -549,6 +687,7 @@ const AgentManagerContent: Component = () => {
<div class="am-empty-state-text">No sessions open</div>
<Button variant="primary" size="small" onClick={handleAddSession}>
New session
<span class="am-shortcut-hint">{modKey}T</span>
</Button>
</div>
</Show>
@@ -118,6 +118,7 @@
/* Worktree item — larger card style */
.am-worktree-item {
position: relative;
display: flex;
align-items: center;
gap: 8px;
@@ -151,6 +152,24 @@
}
.am-worktree-close {
position: absolute;
right: 4px;
flex-shrink: 0;
opacity: 0;
}
.am-worktree-item:hover .am-worktree-branch {
mask-image: linear-gradient(to right, black calc(100% - 48px), transparent calc(100% - 16px));
-webkit-mask-image: linear-gradient(to right, black calc(100% - 48px), transparent calc(100% - 16px));
}
.am-worktree-item:hover .am-worktree-close {
opacity: 1;
}
.am-worktree-spinner {
width: 16px;
height: 16px;
flex-shrink: 0;
}
@@ -168,6 +187,7 @@
/* Session items */
.am-item {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
@@ -219,10 +239,16 @@
/* Promote button on session rows */
.am-item-promote {
position: absolute;
right: 4px;
flex-shrink: 0;
opacity: 0;
}
.am-item:hover .am-item-time {
opacity: 0;
}
.am-item:hover .am-item-promote {
opacity: 1;
}
@@ -341,6 +367,13 @@
align-self: center;
}
.am-tab-terminal {
flex-shrink: 0;
align-self: center;
margin-left: auto;
padding-right: 4px;
}
/* Empty worktree state */
.am-empty-state {
@@ -459,3 +492,59 @@
height: 16px;
flex-shrink: 0;
}
/* Confirmation dialog */
.am-confirm {
display: flex;
flex-direction: column;
gap: 16px;
padding: 0 24px 16px 24px;
}
.am-confirm-message {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: var(--font-size-base);
color: var(--text-base);
line-height: 1.5;
}
.am-confirm-message > [data-component="icon"] {
flex-shrink: 0;
color: var(--icon-critical-base);
margin-top: 2px;
}
.am-confirm-branch {
font-family: var(--font-family-mono, monospace);
font-size: var(--font-size-small);
padding: 1px 6px;
border-radius: var(--radius-sm);
background: var(--surface-inset-base);
white-space: nowrap;
}
.am-confirm-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.am-confirm-delete {
background: var(--surface-critical-strong) !important;
color: var(--text-on-brand-base) !important;
}
.am-confirm-delete:hover {
opacity: 0.9;
}
/* Keyboard shortcut hint inside buttons */
.am-shortcut-hint {
font-size: var(--font-size-small);
opacity: 0.6;
margin-left: 6px;
}
@@ -7,7 +7,10 @@
* Returns the action to take: select a session by ID, go to local, or do nothing.
*/
export type NavResult = { action: "select"; id: string } | { action: "local" } | { action: "none" }
/** Sentinel value for the local workspace selection. */
export const LOCAL = "local" as const
export type NavResult = { action: "select"; id: string } | { action: typeof LOCAL } | { action: "none" }
export function resolveNavigation(direction: "up" | "down", current: string | undefined, ids: string[]): NavResult {
// Determine current position: -1 = local, 0..N-1 = session index
@@ -25,7 +28,7 @@ export function resolveNavigation(direction: "up" | "down", current: string | un
const next = direction === "up" ? idx - 1 : idx + 1
// Moving up past the first session → go to local
if (next === -1) return { action: "local" }
if (next === -1) return { action: LOCAL }
// At the bottom boundary
if (next >= ids.length) return { action: "none" }
@@ -42,3 +45,16 @@ export function validateLocalSession(persisted: string | undefined, ids: string[
if (ids.indexOf(persisted) === -1) return undefined
return persisted
}
/**
* After removing a worktree, pick the nearest remaining sidebar neighbor.
* Order: the worktree just below the one above LOCAL.
*/
export function nextSelectionAfterDelete(deletedId: string, worktreeIds: string[]): typeof LOCAL | string {
const idx = worktreeIds.indexOf(deletedId)
if (idx === -1) return LOCAL
const remaining = worktreeIds.filter((id) => id !== deletedId)
if (remaining.length === 0) return LOCAL
// Prefer the item that was below (same index in the shortened list), else the one above
return remaining[Math.min(idx, remaining.length - 1)]!
}
@@ -70,10 +70,30 @@ export const PromptInput: Component = () => {
const canSend = () => (text().trim().length > 0 || imageAttach.images().length > 0) && !isBusy() && !isDisabled()
const unsubscribe = vscode.onMessage((message) => {
if (message.type !== "chatCompletionResult") return
const result = message as { type: "chatCompletionResult"; text: string; requestId: string }
if (result.requestId === `chat-ac-${requestCounter}` && result.text) {
setGhostText(result.text)
if (message.type === "chatCompletionResult") {
const result = message as { type: "chatCompletionResult"; text: string; requestId: string }
if (result.requestId === `chat-ac-${requestCounter}` && result.text) {
setGhostText(result.text)
}
}
if (message.type === "setChatBoxMessage") {
setText(message.text)
setGhostText("")
if (textareaRef) {
textareaRef.value = message.text
adjustHeight()
}
}
if (message.type === "triggerTask") {
if (isBusy() || isDisabled()) return
const sel = session.selected()
session.sendMessage(message.text, sel?.providerID, sel?.modelID)
}
if (message.type === "action" && message.action === "focusInput") {
textareaRef?.focus()
}
})
@@ -387,6 +387,16 @@ export interface ActionMessage {
action: string
}
export interface SetChatBoxMessage {
type: "setChatBoxMessage"
text: string
}
export interface TriggerTaskMessage {
type: "triggerTask"
text: string
}
export interface ProfileDataMessage {
type: "profileData"
data: ProfileData | null
@@ -541,6 +551,13 @@ export interface ManagedSessionState {
createdAt: string
}
// Agent Manager session added to an existing worktree (no setup overlay needed)
export interface AgentManagerSessionAddedMessage {
type: "agentManager.sessionAdded"
sessionId: string
worktreeId: string
}
// Full state push from extension to webview
export interface AgentManagerStateMessage {
type: "agentManager.state"
@@ -584,7 +601,10 @@ export type ExtensionMessage =
| AgentManagerSessionMetaMessage
| AgentManagerRepoInfoMessage
| AgentManagerWorktreeSetupMessage
| AgentManagerSessionAddedMessage
| AgentManagerStateMessage
| SetChatBoxMessage
| TriggerTaskMessage
// ============================================
// Messages FROM webview TO extension
@@ -813,6 +833,12 @@ export interface RequestRepoInfoMessage {
type: "agentManager.requestRepoInfo"
}
// Show terminal for a session
export interface ShowTerminalRequest {
type: "agentManager.showTerminal"
sessionId: string
}
export type WebviewMessage =
| SendMessageRequest
| AbortRequest
@@ -856,6 +882,7 @@ export type WebviewMessage =
| CloseSessionRequest
| TelemetryRequest
| RequestRepoInfoMessage
| ShowTerminalRequest
// ============================================
// VS Code API type
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.23",
"version": "1.0.24",
"name": "@kilocode/cli",
"type": "module",
"license": "MIT",
@@ -0,0 +1,162 @@
import { describe, expect, test, mock, beforeEach } from "bun:test"
import type { GitContext } from "../types"
// Mock dependencies before importing the module under test
let mockGitContext: GitContext = {
branch: "main",
recentCommits: ["abc1234 initial commit"],
files: [{ status: "modified" as const, path: "src/index.ts", diff: "+console.log('hello')" }],
}
mock.module("../git-context", () => ({
getGitContext: async () => mockGitContext,
}))
let mockStreamText = "feat(src): add hello world logging"
mock.module("@/provider/provider", () => ({
Provider: {
defaultModel: async () => ({ providerID: "test", modelID: "test-model" }),
getSmallModel: async () => ({
providerID: "test",
id: "test-small-model",
}),
getModel: async () => ({ providerID: "test", id: "test-model" }),
},
}))
mock.module("@/session/llm", () => ({
LLM: {
stream: async () => ({
text: Promise.resolve(mockStreamText),
}),
},
}))
mock.module("@/agent/agent", () => ({
Agent: {},
}))
mock.module("@/util/log", () => ({
Log: {
create: () => ({
info: () => {},
error: () => {},
warn: () => {},
debug: () => {},
}),
},
}))
import { generateCommitMessage } from "../generate"
describe("commit-message.generate", () => {
beforeEach(() => {
mockGitContext = {
branch: "main",
recentCommits: ["abc1234 initial commit"],
files: [{ status: "modified" as const, path: "src/index.ts", diff: "+console.log('hello')" }],
}
mockStreamText = "feat(src): add hello world logging"
})
describe("prompt construction", () => {
test("passes path to getGitContext", async () => {
const result = await generateCommitMessage({ path: "/my/repo" })
// If getGitContext is called, it returns our mock context and generates a message
expect(result.message).toBeTruthy()
})
test("generates message from git context with multiple files", async () => {
mockGitContext = {
branch: "feature/api",
recentCommits: ["abc feat: add api", "def fix: typo"],
files: [
{ status: "added" as const, path: "src/api.ts", diff: "+export function api() {}" },
{ status: "modified" as const, path: "src/index.ts", diff: "+import { api } from './api'" },
],
}
mockStreamText = "feat(api): add api module"
const result = await generateCommitMessage({ path: "/repo" })
expect(result.message).toBe("feat(api): add api module")
})
})
describe("response cleaning", () => {
test("strips code block markers from response", async () => {
mockStreamText = "```\nfeat: add feature\n```"
const result = await generateCommitMessage({ path: "/repo" })
expect(result.message).toBe("feat: add feature")
})
test("strips code block markers with language tag", async () => {
mockStreamText = "```text\nfix(auth): resolve token refresh\n```"
const result = await generateCommitMessage({ path: "/repo" })
expect(result.message).toBe("fix(auth): resolve token refresh")
})
test("strips surrounding double quotes", async () => {
mockStreamText = '"feat: add new feature"'
const result = await generateCommitMessage({ path: "/repo" })
expect(result.message).toBe("feat: add new feature")
})
test("strips surrounding single quotes", async () => {
mockStreamText = "'fix: resolve bug'"
const result = await generateCommitMessage({ path: "/repo" })
expect(result.message).toBe("fix: resolve bug")
})
test("strips whitespace around the message", async () => {
mockStreamText = " \n chore: update deps \n "
const result = await generateCommitMessage({ path: "/repo" })
expect(result.message).toBe("chore: update deps")
})
test("strips code blocks AND quotes together", async () => {
mockStreamText = '```\n"refactor: simplify logic"\n```'
const result = await generateCommitMessage({ path: "/repo" })
expect(result.message).toBe("refactor: simplify logic")
})
test("returns clean message when no markers present", async () => {
mockStreamText = "docs: update readme"
const result = await generateCommitMessage({ path: "/repo" })
expect(result.message).toBe("docs: update readme")
})
})
describe("error on no changes", () => {
test("throws when no git changes are found", async () => {
mockGitContext = {
branch: "main",
recentCommits: [],
files: [],
}
await expect(generateCommitMessage({ path: "/repo" })).rejects.toThrow(
"No changes found to generate a commit message for",
)
})
})
describe("selectedFiles pass-through", () => {
test("passes selectedFiles to getGitContext", async () => {
// This verifies the function doesn't crash when selectedFiles is provided
const result = await generateCommitMessage({
path: "/repo",
selectedFiles: ["src/a.ts"],
})
expect(result.message).toBeTruthy()
})
})
})
@@ -0,0 +1,329 @@
import { describe, expect, test, beforeEach } from "bun:test"
// Mock Bun.spawnSync before importing the module under test
const spawnSyncResults: Record<string, string> = {}
function setGitOutput(args: string, output: string) {
spawnSyncResults[args] = output
}
function clearGitOutputs() {
for (const key of Object.keys(spawnSyncResults)) {
delete spawnSyncResults[key]
}
}
// Replace global Bun.spawnSync — the git() helper in git-context.ts calls
// result.stdout.toString().trim(), so we return a Buffer and let git() trim.
Bun.spawnSync = ((cmd: string[], _opts?: any) => {
const args = cmd.slice(1).join(" ")
const output = spawnSyncResults[args] ?? ""
return {
stdout: Buffer.from(output),
stderr: Buffer.from(""),
exitCode: 0,
}
}) as typeof Bun.spawnSync
import { getGitContext } from "../git-context"
describe("commit-message.git-context", () => {
beforeEach(() => {
clearGitOutputs()
// Defaults
setGitOutput("branch --show-current", "main")
setGitOutput("log --oneline -5", "abc1234 initial commit")
setGitOutput("diff --name-status --cached", "")
setGitOutput("status --porcelain", "")
})
// NOTE: git() trims stdout, which eats the leading space of the first
// porcelain line. We use staged (--name-status) tests for path-sensitive
// assertions and only use porcelain for behavior tests where this is acceptable.
describe("lock file filtering", () => {
test("filters out package-lock.json from staged changes", async () => {
setGitOutput("diff --name-status --cached", "M\tsrc/index.ts\nM\tpackage-lock.json")
setGitOutput("diff --cached -- src/index.ts", "+console.log('hello')")
setGitOutput("diff --cached -- package-lock.json", "+lots of lock content")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.path).toBe("src/index.ts")
})
test("filters out yarn.lock from staged changes", async () => {
setGitOutput("diff --name-status --cached", "M\tsrc/app.ts\nM\tyarn.lock")
setGitOutput("diff --cached -- src/app.ts", "+import x")
setGitOutput("diff --cached -- yarn.lock", "+lock data")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.path).toBe("src/app.ts")
})
test("filters out pnpm-lock.yaml from staged changes", async () => {
setGitOutput("diff --name-status --cached", "M\treadme.md\nM\tpnpm-lock.yaml")
setGitOutput("diff --cached -- pnpm-lock.yaml", "+lock")
setGitOutput("diff --cached -- readme.md", "+docs")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.path).toBe("readme.md")
})
test("filters lock files in subdirectories", async () => {
setGitOutput("diff --name-status --cached", "M\tpackages/api/package-lock.json\nM\tpackages/api/src/index.ts")
setGitOutput("diff --cached -- packages/api/package-lock.json", "+lock stuff")
setGitOutput("diff --cached -- packages/api/src/index.ts", "+code")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.path).toBe("packages/api/src/index.ts")
})
test("filters out bun.lockb, go.sum, Cargo.lock, poetry.lock", async () => {
setGitOutput(
"diff --name-status --cached",
"M\tbun.lockb\nM\tgo.sum\nM\tCargo.lock\nM\tpoetry.lock\nM\tsrc/main.rs",
)
setGitOutput("diff --cached -- bun.lockb", "binary")
setGitOutput("diff --cached -- go.sum", "+hash")
setGitOutput("diff --cached -- Cargo.lock", "+lock")
setGitOutput("diff --cached -- poetry.lock", "+lock")
setGitOutput("diff --cached -- src/main.rs", "+fn main() {}")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.path).toBe("src/main.rs")
})
})
describe("status parsing", () => {
test("parses staged added files", async () => {
setGitOutput("diff --name-status --cached", "A\tsrc/new-file.ts")
setGitOutput("diff --cached -- src/new-file.ts", "+new content")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.status).toBe("added")
expect(ctx.files[0]!.path).toBe("src/new-file.ts")
})
test("parses staged modified files", async () => {
setGitOutput("diff --name-status --cached", "M\tsrc/existing.ts")
setGitOutput("diff --cached -- src/existing.ts", "+changed line")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.status).toBe("modified")
})
test("parses staged deleted files", async () => {
setGitOutput("diff --name-status --cached", "D\tsrc/removed.ts")
setGitOutput("diff --cached -- src/removed.ts", "-deleted content")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.status).toBe("deleted")
})
test("parses staged renamed files", async () => {
setGitOutput("diff --name-status --cached", "R100\told-name.ts\tnew-name.ts")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.status).toBe("renamed")
})
test("parses untracked files from porcelain", async () => {
setGitOutput("status --porcelain", "?? src/brand-new.ts")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.status).toBe("added")
expect(ctx.files[0]!.diff).toBe("New untracked file: src/brand-new.ts")
})
test("parses porcelain modified files", async () => {
// Use staged to avoid porcelain trim edge case
setGitOutput("diff --name-status --cached", "M\tsrc/changed.ts")
setGitOutput("diff --cached -- src/changed.ts", "+line")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.status).toBe("modified")
})
test("prefers staged changes over unstaged", async () => {
setGitOutput("diff --name-status --cached", "M\tsrc/staged.ts")
setGitOutput("diff --cached -- src/staged.ts", "+staged change")
// unstaged also exists but should be ignored when staged is present
setGitOutput("status --porcelain", " M src/unstaged.ts")
setGitOutput("diff -- src/unstaged.ts", "+unstaged change")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.path).toBe("src/staged.ts")
})
test("mapStatus returns 'modified' for unknown codes", async () => {
setGitOutput("diff --name-status --cached", "X\tsrc/weird.ts")
setGitOutput("diff --cached -- src/weird.ts", "+stuff")
const ctx = await getGitContext("/repo")
expect(ctx.files[0]!.status).toBe("modified")
})
})
describe("diff truncation", () => {
test("truncates diffs exceeding 4000 characters", async () => {
const longDiff = "x".repeat(5000)
setGitOutput("diff --name-status --cached", "M\tsrc/big.ts")
setGitOutput("diff --cached -- src/big.ts", longDiff)
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.diff.length).toBeLessThan(5000)
expect(ctx.files[0]!.diff).toContain("... [truncated]")
// 4000 chars + "\n... [truncated]"
expect(ctx.files[0]!.diff.length).toBe(4000 + "\n... [truncated]".length)
})
test("does not truncate diffs at exactly 4000 characters", async () => {
const exactDiff = "y".repeat(4000)
setGitOutput("diff --name-status --cached", "M\tsrc/exact.ts")
setGitOutput("diff --cached -- src/exact.ts", exactDiff)
const ctx = await getGitContext("/repo")
expect(ctx.files[0]!.diff).toBe(exactDiff)
expect(ctx.files[0]!.diff).not.toContain("... [truncated]")
})
test("does not truncate diffs under 4000 characters", async () => {
const shortDiff = "z".repeat(100)
setGitOutput("diff --name-status --cached", "M\tsrc/small.ts")
setGitOutput("diff --cached -- src/small.ts", shortDiff)
const ctx = await getGitContext("/repo")
expect(ctx.files[0]!.diff).toBe(shortDiff)
})
})
describe("binary file detection", () => {
test("detects 'Binary files' in diff output", async () => {
setGitOutput("diff --name-status --cached", "M\tassets/logo.png")
setGitOutput("diff --cached -- assets/logo.png", "Binary files a/assets/logo.png and b/assets/logo.png differ")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.diff).toBe("Binary file assets/logo.png has been modified")
})
test("detects 'GIT binary patch' in diff output", async () => {
setGitOutput("diff --name-status --cached", "M\tassets/icon.ico")
setGitOutput("diff --cached -- assets/icon.ico", "GIT binary patch\nliteral 1234\ndata...")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(1)
expect(ctx.files[0]!.diff).toBe("Binary file assets/icon.ico has been modified")
})
test("does not flag normal diffs as binary", async () => {
setGitOutput("diff --name-status --cached", "M\tsrc/code.ts")
setGitOutput("diff --cached -- src/code.ts", "+const x = 1")
const ctx = await getGitContext("/repo")
expect(ctx.files[0]!.diff).toBe("+const x = 1")
})
})
describe("selected files filtering", () => {
test("only includes files in selectedFiles set", async () => {
setGitOutput("diff --name-status --cached", "M\tsrc/a.ts\nM\tsrc/b.ts\nM\tsrc/c.ts")
setGitOutput("diff --cached -- src/a.ts", "+a")
setGitOutput("diff --cached -- src/b.ts", "+b")
setGitOutput("diff --cached -- src/c.ts", "+c")
const ctx = await getGitContext("/repo", ["src/a.ts", "src/c.ts"])
expect(ctx.files).toHaveLength(2)
const paths = ctx.files.map((f) => f.path)
expect(paths).toContain("src/a.ts")
expect(paths).toContain("src/c.ts")
expect(paths).not.toContain("src/b.ts")
})
test("includes all files when selectedFiles is undefined", async () => {
setGitOutput("diff --name-status --cached", "M\tsrc/a.ts\nM\tsrc/b.ts")
setGitOutput("diff --cached -- src/a.ts", "+a")
setGitOutput("diff --cached -- src/b.ts", "+b")
const ctx = await getGitContext("/repo")
expect(ctx.files).toHaveLength(2)
})
test("returns empty files when selectedFiles has no matches", async () => {
setGitOutput("diff --name-status --cached", "M\tsrc/a.ts")
setGitOutput("diff --cached -- src/a.ts", "+a")
const ctx = await getGitContext("/repo", ["src/nonexistent.ts"])
expect(ctx.files).toHaveLength(0)
})
})
describe("branch and recent commits", () => {
test("returns current branch name", async () => {
setGitOutput("branch --show-current", "feature/my-branch")
const ctx = await getGitContext("/repo")
expect(ctx.branch).toBe("feature/my-branch")
})
test("falls back to HEAD when branch is empty", async () => {
setGitOutput("branch --show-current", "")
const ctx = await getGitContext("/repo")
expect(ctx.branch).toBe("HEAD")
})
test("returns recent commits as array", async () => {
setGitOutput("log --oneline -5", "abc1234 first\ndef5678 second\nghi9012 third")
const ctx = await getGitContext("/repo")
expect(ctx.recentCommits).toEqual(["abc1234 first", "def5678 second", "ghi9012 third"])
})
test("returns empty array when no commits", async () => {
setGitOutput("log --oneline -5", "")
const ctx = await getGitContext("/repo")
expect(ctx.recentCommits).toEqual([])
})
})
})
@@ -0,0 +1,178 @@
import { Provider } from "@/provider/provider"
import { LLM } from "@/session/llm"
import { Agent } from "@/agent/agent"
import { Log } from "@/util/log"
import type { CommitMessageRequest, CommitMessageResponse, GitContext } from "./types"
import { getGitContext } from "./git-context"
const log = Log.create({ service: "commit-message" })
const SYSTEM_PROMPT = `You are an expert Git commit message generator that creates conventional commit messages based on staged changes. Analyze the provided git diff output and generate an appropriate conventional commit message following the specification.
## Conventional Commits Format
Generate commit messages following this exact structure:
\`\`\`
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
\`\`\`
### Core Types (Required)
- **feat**: New feature or functionality (MINOR version bump)
- **fix**: Bug fix or error correction (PATCH version bump)
### Additional Types (Extended)
- **docs**: Documentation changes only
- **style**: Code style changes (whitespace, formatting, semicolons, etc.)
- **refactor**: Code refactoring without feature changes or bug fixes
- **perf**: Performance improvements
- **test**: Adding or fixing tests
- **build**: Build system or external dependency changes
- **ci**: CI/CD configuration changes
- **chore**: Maintenance tasks, tooling changes
- **revert**: Reverting previous commits
### Scope Guidelines
- Use parentheses: \`feat(api):\`, \`fix(ui):\`
- Common scopes: \`api\`, \`ui\`, \`auth\`, \`db\`, \`config\`, \`deps\`, \`docs\`
- For monorepos: package or module names
- Keep scope concise and lowercase
### Description Rules
- Use imperative mood ("add" not "added" or "adds")
- Start with lowercase letter
- No period at the end
- Maximum 72 characters
- Be concise but descriptive
### Body Guidelines (Optional)
- Start one blank line after description
- Explain the "what" and "why", not the "how"
- Wrap at 72 characters per line
- Use for complex changes requiring explanation
### Footer Guidelines (Optional)
- Start one blank line after body
- **Breaking Changes**: \`BREAKING CHANGE: description\`
## Analysis Instructions
When analyzing staged changes:
1. Determine Primary Type based on the nature of changes
2. Identify Scope from modified directories or modules
3. Craft Description focusing on the most significant change
4. Determine if there are Breaking Changes
5. For complex changes, include a detailed body explaining what and why
6. Add appropriate footers for issue references or breaking changes
For significant changes, include a detailed body explaining the changes.
Return ONLY the commit message in the conventional format, nothing else.`
function buildUserMessage(ctx: GitContext): string {
const fileList = ctx.files.map((f) => `${f.status} ${f.path}`).join("\n")
const diffs = ctx.files
.filter((f) => f.diff)
.map((f) => `--- ${f.path} ---\n${f.diff}`)
.join("\n\n")
return `Generate a commit message for the following changes:
Branch: ${ctx.branch}
Recent commits:
${ctx.recentCommits.join("\n")}
Changed files:
${fileList}
Diffs:
${diffs}`
}
function clean(text: string): string {
let result = text.trim()
// Strip code block markers
if (result.startsWith("```")) {
const first = result.indexOf("\n")
if (first !== -1) {
result = result.slice(first + 1)
}
}
if (result.endsWith("```")) {
result = result.slice(0, -3)
}
result = result.trim()
// Strip surrounding quotes
if ((result.startsWith('"') && result.endsWith('"')) || (result.startsWith("'") && result.endsWith("'"))) {
result = result.slice(1, -1)
}
return result.trim()
}
export async function generateCommitMessage(request: CommitMessageRequest): Promise<CommitMessageResponse> {
const ctx = await getGitContext(request.path, request.selectedFiles)
if (ctx.files.length === 0) {
throw new Error("No changes found to generate a commit message for")
}
log.info("generating", {
branch: ctx.branch,
files: ctx.files.length,
})
const defaultModel = await Provider.defaultModel()
const model =
(await Provider.getSmallModel(defaultModel.providerID)) ??
(await Provider.getModel(defaultModel.providerID, defaultModel.modelID))
const agent: Agent.Info = {
name: "commit-message",
mode: "primary",
hidden: true,
options: {},
permission: [],
prompt: SYSTEM_PROMPT,
temperature: 0.3,
}
let userMessage = buildUserMessage(ctx)
if (request.previousMessage) {
userMessage = `IMPORTANT: Generate a COMPLETELY DIFFERENT commit message from the previous one. The previous message was: "${request.previousMessage}". Use a different type, scope, or description approach.\n\n${userMessage}`
}
const stream = await LLM.stream({
agent,
user: {
id: "commit-message",
sessionID: "commit-message",
role: "user",
model: {
providerID: model.providerID,
modelID: model.id,
},
time: {
created: Date.now(),
completed: Date.now(),
},
} as any,
tools: {},
model,
small: true,
messages: [
{
role: "user" as const,
content: userMessage,
},
],
abort: new AbortController().signal,
sessionID: "commit-message",
system: [],
retries: 3,
})
const result = await stream.text
log.info("generated", { message: result })
return { message: clean(result) }
}
@@ -0,0 +1,230 @@
import type { GitContext, FileChange } from "./types"
const LOCK_FILES = new Set([
// --- JavaScript / Node.js ---
"package-lock.json",
"npm-shrinkwrap.json",
"yarn.lock",
"pnpm-lock.yaml",
"shrinkwrap.yaml",
"bun.lockb",
"bun.lock",
".pnp.js",
".pnp.cjs",
"jspm.lock",
// --- Python ---
"Pipfile.lock",
"poetry.lock",
"pdm.lock",
".pdm-lock.toml",
"uv.lock",
"conda-lock.yml",
"pylock.toml",
// --- Ruby ---
"Gemfile.lock",
// --- PHP ---
"composer.lock",
// --- Java / JVM ---
"gradle.lockfile",
"lockfile.json",
"dependency-lock.json",
"dependency-reduced-pom.xml",
"coursier.lock",
// --- Scala ---
"build.sbt.lock",
// --- .NET ---
"packages.lock.json",
"paket.lock",
"project.assets.json",
// --- Rust ---
"Cargo.lock",
// --- Go ---
"go.sum",
"Gopkg.lock",
"glide.lock",
// --- Zig ---
"build.zig.zon.lock",
// --- OCaml ---
"dune.lock",
"opam.lock",
// --- Swift / iOS ---
"Package.resolved",
"Podfile.lock",
"Cartfile.resolved",
// --- Dart / Flutter ---
"pubspec.lock",
// --- Elixir / Erlang ---
"mix.lock",
"rebar.lock",
// --- Haskell ---
"stack.yaml.lock",
"cabal.project.freeze",
// --- Elm ---
"exact-dependencies.json",
// --- Crystal ---
"shard.lock",
// --- Julia ---
"Manifest.toml",
"JuliaManifest.toml",
// --- R ---
"renv.lock",
"packrat.lock",
// --- Nim ---
"nimble.lock",
// --- D ---
"dub.selections.json",
// --- Lua ---
"rocks.lock",
// --- Perl ---
"carton.lock",
"cpanfile.snapshot",
// --- C/C++ ---
"conan.lock",
"vcpkg-lock.json",
// --- Infrastructure as Code ---
".terraform.lock.hcl",
"Berksfile.lock",
"Puppetfile.lock",
"MODULE.bazel.lock",
// --- Nix ---
"flake.lock",
// --- Deno ---
"deno.lock",
// --- DevContainers ---
"devcontainer.lock.json",
])
const MAX_DIFF_LENGTH = 4000
function isLockFile(filepath: string): boolean {
const name = filepath.split("/").pop() ?? filepath
return LOCK_FILES.has(name)
}
function git(args: string[], cwd: string): string {
const result = Bun.spawnSync(["git", ...args], {
cwd,
stdout: "pipe",
stderr: "pipe",
})
return result.stdout.toString().trimEnd()
}
function parseNameStatus(output: string): Array<{ status: string; path: string }> {
if (!output) return []
return output.split("\n").map((line) => {
const [status, ...rest] = line.split("\t")
let path: string
if (status!.startsWith("R")) {
// Rename: rest = ["old.ts", "new.ts"], use the new path
path = rest[1] ?? rest[0]
} else {
path = rest.join("\t")
}
return { status: status!, path }
})
}
function parsePorcelain(output: string): Array<{ status: string; path: string }> {
if (!output) return []
return output
.split("\n")
.filter((line) => line.length > 0)
.map((line) => {
const xy = line.slice(0, 2)
const filepath = line.slice(3)
return { status: xy.trim(), path: filepath }
})
}
function mapStatus(code: string): FileChange["status"] {
if (code.startsWith("R")) return "renamed"
if (code === "A" || code === "??" || code === "?") return "added"
if (code === "D") return "deleted"
if (code === "M") return "modified"
return "modified"
}
function isUntracked(code: string): boolean {
return code === "??" || code === "?"
}
export async function getGitContext(repoPath: string, selectedFiles?: string[]): Promise<GitContext> {
const branch = git(["branch", "--show-current"], repoPath) || "HEAD"
const log = git(["log", "--oneline", "-5"], repoPath)
const recentCommits = log ? log.split("\n") : []
// Check staged files first
const staged = parseNameStatus(git(["diff", "--name-status", "--cached"], repoPath))
const useStaged = staged.length > 0
// Fall back to all changes if nothing staged
const raw = useStaged ? staged : parsePorcelain(git(["status", "--porcelain"], repoPath))
const selected = selectedFiles ? new Set(selectedFiles) : undefined
const files: FileChange[] = []
for (const entry of raw) {
if (isLockFile(entry.path)) continue
if (selected && !selected.has(entry.path)) continue
const status = mapStatus(entry.status)
const untracked = isUntracked(entry.status)
let diff: string
if (untracked) {
diff = `New untracked file: ${entry.path}`
} else if (status === "deleted") {
diff = useStaged
? git(["diff", "--cached", "--", entry.path], repoPath)
: git(["diff", "--", entry.path], repoPath)
} else {
const raw = useStaged
? git(["diff", "--cached", "--", entry.path], repoPath)
: git(["diff", "--", entry.path], repoPath)
// Detect binary files
if (raw.includes("Binary files") || raw.includes("GIT binary patch")) {
diff = `Binary file ${entry.path} has been modified`
} else {
diff = raw
}
}
// Truncate large diffs
if (diff.length > MAX_DIFF_LENGTH) {
diff = diff.slice(0, MAX_DIFF_LENGTH) + "\n... [truncated]"
}
files.push({ status, path: entry.path, diff })
}
return { branch, recentCommits, files }
}
@@ -0,0 +1,2 @@
export { generateCommitMessage } from "./generate"
export type { CommitMessageRequest, CommitMessageResponse, GitContext, FileChange } from "./types"
@@ -0,0 +1,29 @@
export interface CommitMessageRequest {
/** Workspace/repo path */
path: string
/** Optional subset of files to include */
selectedFiles?: string[]
/** Previously generated message — when set, the LLM is asked to produce a different one */
previousMessage?: string
}
export interface CommitMessageResponse {
/** The generated commit message */
message: string
}
export interface GitContext {
/** Current branch name */
branch: string
/** Last 5 commit summaries */
recentCommits: string[]
/** File changes with status and diff content */
files: FileChange[]
}
export interface FileChange {
status: "added" | "modified" | "deleted" | "renamed"
path: string
/** Diff content, or placeholder for binary/untracked files */
diff: string
}
@@ -0,0 +1,44 @@
import { Hono } from "hono"
import { describeRoute, resolver, validator } from "hono-openapi"
import z from "zod"
import { generateCommitMessage } from "../../commit-message"
import { lazy } from "../../util/lazy"
import { errors } from "../error"
export const CommitMessageRoutes = lazy(() =>
new Hono().post(
"/",
describeRoute({
summary: "Generate commit message",
description: "Generate a commit message using AI based on the current git diff.",
operationId: "commitMessage.generate",
responses: {
200: {
description: "Generated commit message",
content: {
"application/json": {
schema: resolver(z.object({ message: z.string() })),
},
},
},
...errors(400),
},
}),
validator(
"json",
z.object({
path: z.string().meta({ description: "Workspace/repo path" }),
selectedFiles: z.array(z.string()).optional().meta({ description: "Optional subset of files to include" }),
previousMessage: z
.string()
.optional()
.meta({ description: "Previously generated message — triggers regeneration with a different result" }),
}),
),
async (c) => {
const body = c.req.valid("json")
const result = await generateCommitMessage(body)
return c.json({ message: result.message })
},
),
)
+2
View File
@@ -38,6 +38,7 @@ import type { ContentfulStatusCode } from "hono/utils/http-status"
import { websocket } from "hono/bun"
import { HTTPException } from "hono/http-exception"
import { errors } from "./error"
import { CommitMessageRoutes } from "./routes/commit-message"
import { QuestionRoutes } from "./routes/question"
import { PermissionRoutes } from "./routes/permission"
import { GlobalRoutes } from "./routes/global"
@@ -235,6 +236,7 @@ export namespace Server {
.route("/question", QuestionRoutes())
.route("/provider", ProviderRoutes())
.route("/telemetry", TelemetryRoutes()) // kilocode_change
.route("/commit-message", CommitMessageRoutes()) // kilocode_change
// kilocode_change start - Kilo Gateway routes
.route(
"/kilo",
+3 -2
View File
@@ -26,7 +26,7 @@ import { DEFAULT_HEADERS } from "@/kilocode/const" // kilocode_change
import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change
// kilocode_change start
import { getKiloProjectId } from "@/kilocode/project-id"
import { HEADER_PROJECTID, HEADER_MACHINEID } from "@kilocode/kilo-gateway"
import { HEADER_PROJECTID, HEADER_MACHINEID, HEADER_TASKID } from "@kilocode/kilo-gateway"
import { Identity } from "@kilocode/kilo-telemetry"
// kilocode_change end
@@ -239,9 +239,10 @@ export namespace LLM {
? DEFAULT_HEADERS // kilocode_change
: undefined),
...(isKilo && input.agent.name ? { "x-kilocode-mode": input.agent.name.toLowerCase() } : {}),
// kilocode_change start - add project ID and machine ID headers for kilo provider
// kilocode_change start - add project ID, machine ID, and task ID headers for kilo provider
...(isKilo && kiloProjectId ? { [HEADER_PROJECTID]: kiloProjectId } : {}),
...(isKilo && machineId ? { [HEADER_MACHINEID]: machineId } : {}),
...(isKilo ? { [HEADER_TASKID]: input.sessionID } : {}),
// kilocode_change end
...input.model.headers,
...headers,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/plugin",
"version": "1.0.23",
"version": "1.0.24",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -8,7 +8,7 @@
"exports": {
".": "./src/index.ts"
},
"version": "1.0.23",
"version": "1.0.24",
"dependencies": {},
"peerDependencies": {}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/sdk",
"version": "1.0.23",
"version": "1.0.24",
"type": "module",
"license": "MIT",
"repository": {
+52
View File
@@ -14,6 +14,8 @@ import type {
AuthSetErrors,
AuthSetResponses,
CommandListResponses,
CommitMessageGenerateErrors,
CommitMessageGenerateResponses,
Config as Config3,
ConfigGetResponses,
ConfigProvidersResponses,
@@ -2217,6 +2219,51 @@ export class Telemetry extends HeyApiClient {
}
}
export class CommitMessage extends HeyApiClient {
/**
* Generate commit message
*
* Generate a commit message using AI based on the current git diff.
*/
public generate<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
path?: string
selectedFiles?: Array<string>
previousMessage?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "body", key: "path" },
{ in: "body", key: "selectedFiles" },
{ in: "body", key: "previousMessage" },
],
},
],
)
return (options?.client ?? this.client).post<
CommitMessageGenerateResponses,
CommitMessageGenerateErrors,
ThrowOnError
>({
url: "/commit-message",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}
export class Organization extends HeyApiClient {
/**
* Update Kilo Gateway organization
@@ -3439,6 +3486,11 @@ export class OpencodeClient extends HeyApiClient {
return (this._telemetry ??= new Telemetry({ client: this.client }))
}
private _commitMessage?: CommitMessage
get commitMessage(): CommitMessage {
return (this._commitMessage ??= new CommitMessage({ client: this.client }))
}
private _kilo?: Kilo
get kilo(): Kilo {
return (this._kilo ??= new Kilo({ client: this.client }))
+42
View File
@@ -4258,6 +4258,48 @@ export type TelemetryCaptureResponses = {
export type TelemetryCaptureResponse = TelemetryCaptureResponses[keyof TelemetryCaptureResponses]
export type CommitMessageGenerateData = {
body?: {
/**
* Workspace/repo path
*/
path: string
/**
* Optional subset of files to include
*/
selectedFiles?: Array<string>
/**
* Previously generated message triggers regeneration with a different result
*/
previousMessage?: string
}
path?: never
query?: {
directory?: string
}
url: "/commit-message"
}
export type CommitMessageGenerateErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type CommitMessageGenerateError = CommitMessageGenerateErrors[keyof CommitMessageGenerateErrors]
export type CommitMessageGenerateResponses = {
/**
* Generated commit message
*/
200: {
message: string
}
}
export type CommitMessageGenerateResponse = CommitMessageGenerateResponses[keyof CommitMessageGenerateResponses]
export type KiloProfileData = {
body?: never
path?: never
+77
View File
@@ -4122,6 +4122,83 @@
]
}
},
"/commit-message": {
"post": {
"operationId": "commitMessage.generate",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
}
],
"summary": "Generate commit message",
"description": "Generate a commit message using AI based on the current git diff.",
"responses": {
"200": {
"description": "Generated commit message",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": ["message"]
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"path": {
"description": "Workspace/repo path",
"type": "string"
},
"selectedFiles": {
"description": "Optional subset of files to include",
"type": "array",
"items": {
"type": "string"
}
},
"previousMessage": {
"description": "Previously generated message — triggers regeneration with a different result",
"type": "string"
}
},
"required": ["path"]
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.commitMessage.generate({\n ...\n})"
}
]
}
},
"/kilo/profile": {
"get": {
"operationId": "kilo.profile",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.23",
"version": "1.0.24",
"type": "module",
"license": "MIT",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.0.23",
"version": "1.0.24",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/upstream-merge",
"version": "1.0.23",
"version": "1.0.24",
"private": true,
"type": "module",
"description": "Scripts for automating upstream opencode merges into Kilo",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.23",
"version": "1.0.24",
"publisher": "sst-dev",
"repository": {
"type": "git",