Merge remote-tracking branch 'origin/main' into fix/costs-of-subagents

This commit is contained in:
Alex Alecu
2026-04-27 13:03:15 +03:00
273 changed files with 13875 additions and 6653 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Prefer ChatGPT OAuth credentials over inherited OpenAI environment variables and make ChatGPT sign-in easier to find.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Add Settings header buttons to open the project and global Kilo config files directly in VS Code.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Prompt before agents access files outside the active directory when a workspace boundary resolves to a filesystem root.
+271 -10
View File
@@ -5,21 +5,22 @@
"packages": {
"": {
"dependencies": {
"@kilocode/plugin": "7.2.14"
"@kilocode/plugin": "7.2.21"
}
},
"node_modules/@kilocode/plugin": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.14.tgz",
"integrity": "sha512-mS+WA9HZIBH2qQ9ARA+v0q4MdQTSdfOvKbe4AOSkjP+P5hVA70OM/UVM9DVcvmjSOxU+wuUxmOy+j/EQIrgFmw==",
"version": "7.2.21",
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.21.tgz",
"integrity": "sha512-PUo1Vc7OmOHZAxOdrFsez01e+JAX7jhyGw0caBOuXwAWuT39tGISjrUQoeW5k2/VqkZv3V8fPyvcuto0iuNeDA==",
"license": "MIT",
"dependencies": {
"@kilocode/sdk": "7.2.14",
"@kilocode/sdk": "7.2.21",
"effect": "4.0.0-beta.48",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.1.97",
"@opentui/solid": ">=0.1.97"
"@opentui/core": ">=0.1.100",
"@opentui/solid": ">=0.1.100"
},
"peerDependenciesMeta": {
"@opentui/core": {
@@ -31,14 +32,98 @@
}
},
"node_modules/@kilocode/sdk": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.14.tgz",
"integrity": "sha512-Naz83lFrsbavuDp6UwxRuglOaSNvRBsZfcRNvb7RpWYAwbuJP0dBdhpXj6uO3ta5qxeQ2JzxKNC9Ffz+LCLLDg==",
"version": "7.2.21",
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.21.tgz",
"integrity": "sha512-iqhUEAEF4ouqXdlm3CuVCCh07FhMpjVT/QfZqDFAis7PO0JTmR76sCAMtqSzv8nogt/CFuR/vgpK79d+xZcM2g==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
}
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -53,12 +138,135 @@
"node": ">= 8"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/effect": {
"version": "4.0.0-beta.48",
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz",
"integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"fast-check": "^4.6.0",
"find-my-way-ts": "^0.1.6",
"ini": "^6.0.0",
"kubernetes-types": "^1.30.0",
"msgpackr": "^1.11.9",
"multipasta": "^0.2.7",
"toml": "^4.1.1",
"uuid": "^13.0.0",
"yaml": "^2.8.3"
}
},
"node_modules/fast-check": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz",
"integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^8.0.0"
},
"engines": {
"node": ">=12.17.0"
}
},
"node_modules/find-my-way-ts": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz",
"integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==",
"license": "MIT"
},
"node_modules/ini": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
"integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/kubernetes-types": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz",
"integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==",
"license": "Apache-2.0"
},
"node_modules/msgpackr": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz",
"integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==",
"license": "MIT",
"optionalDependencies": {
"msgpackr-extract": "^3.0.2"
}
},
"node_modules/msgpackr-extract": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build-optional-packages": "5.2.2"
},
"bin": {
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
},
"optionalDependencies": {
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
}
},
"node_modules/multipasta": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz",
"integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==",
"license": "MIT"
},
"node_modules/node-gyp-build-optional-packages": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.1"
},
"bin": {
"node-gyp-build-optional-packages": "bin.js",
"node-gyp-build-optional-packages-optional": "optional.js",
"node-gyp-build-optional-packages-test": "build-test.js"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -68,6 +276,22 @@
"node": ">=8"
}
},
"node_modules/pure-rand": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz",
"integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -89,6 +313,28 @@
"node": ">=8"
}
},
"node_modules/toml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz",
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -104,6 +350,21 @@
"node": ">= 8"
}
},
"node_modules/yaml": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
+271 -10
View File
@@ -5,21 +5,22 @@
"packages": {
"": {
"dependencies": {
"@kilocode/plugin": "7.2.14"
"@kilocode/plugin": "7.2.21"
}
},
"node_modules/@kilocode/plugin": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.14.tgz",
"integrity": "sha512-mS+WA9HZIBH2qQ9ARA+v0q4MdQTSdfOvKbe4AOSkjP+P5hVA70OM/UVM9DVcvmjSOxU+wuUxmOy+j/EQIrgFmw==",
"version": "7.2.21",
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.21.tgz",
"integrity": "sha512-PUo1Vc7OmOHZAxOdrFsez01e+JAX7jhyGw0caBOuXwAWuT39tGISjrUQoeW5k2/VqkZv3V8fPyvcuto0iuNeDA==",
"license": "MIT",
"dependencies": {
"@kilocode/sdk": "7.2.14",
"@kilocode/sdk": "7.2.21",
"effect": "4.0.0-beta.48",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.1.97",
"@opentui/solid": ">=0.1.97"
"@opentui/core": ">=0.1.100",
"@opentui/solid": ">=0.1.100"
},
"peerDependenciesMeta": {
"@opentui/core": {
@@ -31,14 +32,98 @@
}
},
"node_modules/@kilocode/sdk": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.14.tgz",
"integrity": "sha512-Naz83lFrsbavuDp6UwxRuglOaSNvRBsZfcRNvb7RpWYAwbuJP0dBdhpXj6uO3ta5qxeQ2JzxKNC9Ffz+LCLLDg==",
"version": "7.2.21",
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.21.tgz",
"integrity": "sha512-iqhUEAEF4ouqXdlm3CuVCCh07FhMpjVT/QfZqDFAis7PO0JTmR76sCAMtqSzv8nogt/CFuR/vgpK79d+xZcM2g==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
}
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -53,12 +138,135 @@
"node": ">= 8"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/effect": {
"version": "4.0.0-beta.48",
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz",
"integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"fast-check": "^4.6.0",
"find-my-way-ts": "^0.1.6",
"ini": "^6.0.0",
"kubernetes-types": "^1.30.0",
"msgpackr": "^1.11.9",
"multipasta": "^0.2.7",
"toml": "^4.1.1",
"uuid": "^13.0.0",
"yaml": "^2.8.3"
}
},
"node_modules/fast-check": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz",
"integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^8.0.0"
},
"engines": {
"node": ">=12.17.0"
}
},
"node_modules/find-my-way-ts": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz",
"integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==",
"license": "MIT"
},
"node_modules/ini": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
"integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/kubernetes-types": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz",
"integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==",
"license": "Apache-2.0"
},
"node_modules/msgpackr": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz",
"integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==",
"license": "MIT",
"optionalDependencies": {
"msgpackr-extract": "^3.0.2"
}
},
"node_modules/msgpackr-extract": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build-optional-packages": "5.2.2"
},
"bin": {
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
},
"optionalDependencies": {
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
}
},
"node_modules/multipasta": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz",
"integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==",
"license": "MIT"
},
"node_modules/node-gyp-build-optional-packages": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.1"
},
"bin": {
"node-gyp-build-optional-packages": "bin.js",
"node-gyp-build-optional-packages-optional": "optional.js",
"node-gyp-build-optional-packages-test": "build-test.js"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -68,6 +276,22 @@
"node": ">=8"
}
},
"node_modules/pure-rand": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz",
"integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -89,6 +313,28 @@
"node": ">=8"
}
},
"node_modules/toml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz",
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -104,6 +350,21 @@
"node": ">= 8"
}
},
"node_modules/yaml": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
+10
View File
@@ -20,6 +20,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang
- **Source links**: After adding or changing URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`, run `bun run script/extract-source-links.ts` from the repo root and commit the updated `packages/kilo-docs/source-links.md`. CI runs this check — the build fails if the file is stale.
- **kilocode_change check**: `bun run check-kilocode-change` from `packages/kilo-vscode/`. CI runs this — `kilocode_change` is a marker for upstream merge conflicts and must not appear in `packages/kilo-vscode/` or `packages/kilo-ui/` (these are entirely Kilo Code additions). Remove the markers before pushing.
- **opencode annotation check**: `bun run script/check-opencode-annotations.ts` from repo root. CI runs this on PRs touching `packages/opencode/` — every Kilo-specific change in shared opencode files must be annotated with `kilocode_change` markers. Exempt paths (no markers needed): `packages/opencode/src/kilocode/`, `packages/opencode/test/kilocode/`, and any path containing `kilocode` in the name.
- **Backend/SDK programmatic testing**: see [TESTING.md](./TESTING.md) for spawning the local main-branch backend (`bun dev serve`) and driving it via `curl` — use this instead of `kilo serve` (prod binary) when testing backend fixes.
## Products
@@ -181,6 +182,15 @@ Changeset descriptions appear directly in release notes and are read by end user
PR descriptions should be 2-3 lines covering **what** changed and **why**. Focus on intent and context a reviewer can't get from the diff — skip file-by-file inventories, test result summaries, and anything obvious from the code itself.
## GitHub Issues
- When creating a GitHub issue for the VS Code extension or JetBrains plugin, use the repo's existing issue templates in `.github/ISSUE_TEMPLATE/`. Pick the matching template (`Bug report`, `Feature Request`, or `Question`) instead of opening a blank issue.
- Do not add platform-specific title prefixes such as `[JetBrains]`, `[Jetbrains]`, `[JB]`, `[VS Code]`, `[VSCode]`, or similar. Use a plain, descriptive title.
- Always add VS Code extension issues to the GitHub project `VS Code Extension`: https://github.com/orgs/Kilo-Org/projects/25
- Always add JetBrains plugin issues to the GitHub project `Jetbrains Plugin`: https://github.com/orgs/Kilo-Org/projects/39
- When using `gh`, prefer `gh issue create --template "..." --project "..."` with the matching project title.
- If project assignment fails because `gh` is missing the required scope, run `gh auth refresh -s project` and retry.
## Fork Merge Process
Kilo CLI is a fork of [opencode](https://github.com/anomalyco/opencode).
+3 -1
View File
@@ -6,7 +6,9 @@
<a href="https://www.reddit.com/r/kilocode/"><img src="https://raster.shields.io/badge/Join%20r%2Fkilocode-D84315?style=flat&logo=reddit&logoColor=white" alt="Reddit" height="20"></a>
</p>
# 🚀 Kilo
<p align="center">
<img width="250" alt="kilo-code-logo" src="https://github.com/user-attachments/assets/bdb0c174-b9fd-40ad-a47b-f3aab9b54e8d" />
</p>
> Kilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.
+261
View File
@@ -0,0 +1,261 @@
# TESTING.md
How to spin up the **local main-branch** Kilo backend and test it with `curl` / `fetch`. Aimed at a running Kilo CLI agent iterating on backend fixes without rebuilding the VS Code extension, TUI, or desktop app.
All examples use plain shell + `curl`. Writing TypeScript files is a last resort (see Section 8).
## TL;DR
```bash
# From repo root. Starts the LOCAL main-branch backend in the background.
PASS=$(openssl rand -hex 16)
KILO_SERVER_PASSWORD="$PASS" bun dev serve --port 0 >/tmp/kilo-serve.log 2>&1 &
echo $! >/tmp/kilo-serve.pid
while ! grep -q "kilo server listening" /tmp/kilo-serve.log 2>/dev/null; do sleep 0.1; done
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/kilo-serve.log | grep -oE "[0-9]+$")
AUTH="Authorization: Basic $(printf 'kilo:%s' "$PASS" | base64 | tr -d '\n')"
BASE="http://127.0.0.1:$PORT"
# Call any endpoint
curl -sS -H "$AUTH" -H "x-kilo-directory: $PWD" "$BASE/global/health"
# Stop when done
kill "$(cat /tmp/kilo-serve.pid)" 2>/dev/null || true
rm -f /tmp/kilo-serve.pid /tmp/kilo-serve.log
```
## 1. What this doc is for
Testing local backend fixes against a real running server, talking to it over HTTP the same way the VS Code extension, TUI, and `kilo run --attach` do — but without any of those clients. Every request is a `curl` the agent can copy-paste.
For in-process tests (no socket, fastest loop) see `packages/opencode/test/kilocode/server/permission-allow-everything.test.ts` for the `Server.Default().app.request(...)` pattern. That's the right tool inside the `packages/opencode/` test suite; this doc is for out-of-process HTTP testing.
## 2. `kilo serve` vs `bun dev serve` — important
| Command | What it runs |
|---|---|
| `kilo serve` | The npm-installed production CLI on `$PATH`. **Not the code in this repo.** |
| `bun dev serve …` (repo root) | The local main-branch backend from this worktree. **This is what you want.** |
| `bun run --cwd packages/opencode --conditions=browser src/index.ts serve …` | Same as `bun dev serve`, fully expanded. |
Root `package.json` defines `"dev"` as the full `bun run --cwd packages/opencode --conditions=browser src/index.ts` invocation, so `bun dev <args>` forwards `<args>` to the local CLI entry point (`packages/opencode/src/index.ts`) without touching the installed binary.
`bun dev` imports the source directly — no rebuild is needed between code edits. Just kill the running server and relaunch.
Do **not** use `createKiloServer()` from `@kilocode/sdk/v2` to test local code: it spawns the PATH `kilo` binary (`packages/sdk/js/src/v2/server.ts:38-136`), which is the wrong tool here.
## 3. Starting the backend (background)
### Random port (recommended)
```bash
KILO_SERVER_PASSWORD=$(openssl rand -hex 16) \
bun dev serve --port 0 >/tmp/kilo-serve.log 2>&1 &
echo $! >/tmp/kilo-serve.pid
while ! grep -q "kilo server listening" /tmp/kilo-serve.log; do sleep 0.1; done
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/kilo-serve.log | grep -oE "[0-9]+$")
```
### Fixed port (if you need a stable URL)
```bash
KILO_SERVER_PASSWORD=secret \
bun dev serve --port 4096 --hostname 127.0.0.1 \
>/tmp/kilo-serve.log 2>&1 &
echo $! >/tmp/kilo-serve.pid
while ! grep -q "kilo server listening" /tmp/kilo-serve.log; do sleep 0.1; done
PORT=4096
```
### No-auth quickstart (fastest)
Omit `KILO_SERVER_PASSWORD` entirely. The server prints `Warning: KILO_SERVER_PASSWORD is not set; server is unsecured.` and the auth middleware is bypassed — fine for throwaway local testing, never for anything else.
```bash
bun dev serve --port 0 >/tmp/kilo-serve.log 2>&1 &
echo $! >/tmp/kilo-serve.pid
while ! grep -q "kilo server listening" /tmp/kilo-serve.log; do sleep 0.1; done
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/kilo-serve.log | grep -oE "[0-9]+$")
BASE="http://127.0.0.1:$PORT"
# no AUTH var needed
```
### Flags (`packages/opencode/src/cli/network.ts`)
| Flag | Default | Notes |
|---|---|---|
| `--port` | `0` (OS-assigned) | Must be passed literally when overriding `opencode.json`'s `server.port`. |
| `--hostname` | `127.0.0.1` | Becomes `0.0.0.0` when `--mdns` is set without an override. |
| `--mdns` | `false` | Publishes an mDNS SRV record. |
| `--mdns-domain` | `kilo.local` | |
| `--cors` | `[]` | Extra allowed origins. |
## 4. The two mandatory request knobs
### Auth header (only if `KILO_SERVER_PASSWORD` was set)
```bash
AUTH="Authorization: Basic $(printf 'kilo:%s' "$KILO_SERVER_PASSWORD" | base64 | tr -d '\n')"
```
The username is literally `kilo` (same as the VS Code extension). Skip this whole block if you launched without a password.
### Directory header on every call
```bash
DIR_HEADER="x-kilo-directory: $PWD"
```
`InstanceMiddleware` uses this to scope the request to a project. For `GET` / `HEAD`, pass `?directory=<urlencoded>` in the URL instead — that's what the SDK does internally.
## 5. Common `curl` recipes
All of the below assume `BASE`, `AUTH`, `DIR_HEADER` are set. Drop `-H "$AUTH"` if you're running without a password.
### Health check (no auth, no directory)
```bash
curl -sS "$BASE/global/health"
```
### Full endpoint list (OpenAPI spec)
```bash
curl -sS "$BASE/doc" | jq .
```
This is the source of truth — anything not in this doc is discoverable from `/doc` without reading source.
### Create a session
```bash
SID=$(curl -sS -X POST "$BASE/session" \
-H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
-d '{}' | jq -r .id)
echo "$SID"
```
### List sessions (GET — directory goes in the query)
```bash
curl -sS -H "$AUTH" \
"$BASE/session?directory=$(printf %s "$PWD" | jq -sRr @uri)"
```
### Send a message (fire-and-forget)
```bash
curl -sS -X POST "$BASE/session/$SID/prompt_async" \
-H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
-d '{"parts":[{"type":"text","text":"hello"}]}'
```
### Read messages for a session
```bash
curl -sS -H "$AUTH" \
"$BASE/session/$SID/message?directory=$(printf %s "$PWD" | jq -sRr @uri)"
```
### Abort an in-flight prompt
```bash
curl -sS -X POST -H "$AUTH" -H "$DIR_HEADER" "$BASE/session/$SID/abort"
```
### Get the resolved config (verify a config change took effect)
```bash
curl -sS -H "$AUTH" \
"$BASE/config?directory=$(printf %s "$PWD" | jq -sRr @uri)"
```
### Stream global events (SSE)
```bash
curl -N -sS -H "$AUTH" \
"$BASE/global/event?directory=$(printf %s "$PWD" | jq -sRr @uri)"
```
`-N` disables curl's output buffering so events appear live. Expect lines like `data: {"directory":"…","payload":{"type":"…",…}}`.
## 6. Stopping the backend
```bash
kill "$(cat /tmp/kilo-serve.pid)" 2>/dev/null || true
rm -f /tmp/kilo-serve.pid /tmp/kilo-serve.log
```
`ServeCommand` handles `SIGTERM` / `SIGINT` / `SIGHUP` and runs `Instance.disposeAll()` + `server.stop(true)` before exiting (`packages/opencode/src/cli/cmd/serve.ts:29-31`). `-9` is only needed if the process hangs past ~5 s.
## 7. Useful environment variables
| Var | Why you'd set it |
|---|---|
| `KILO_SERVER_PASSWORD` | Enable Basic auth. Omit for auth-bypassed local testing. |
| `KILO_DB=":memory:"` | Skip on-disk SQLite — hermetic runs. |
| `KILO_DISABLE_DEFAULT_PLUGINS=true` | Don't auto-load bundled plugins. |
| `KILO_WORKSPACE_ID=<id>` | Single-workspace mode; disables control-plane routes. |
| `KILO_TELEMETRY_LEVEL=off` | Disable PostHog during tests. |
| `KILO_CONFIG_CONTENT='{…}'` | Inline JSON config without writing a file. |
## 8. Last resort: typed SDK via a throwaway script
Use this only when `curl` can't express what you need — typed request/response shapes, complex multi-turn orchestration, SSE consumers that need to coalesce events. Keep the file short and delete it after.
```ts
// /tmp/probe.ts — delete after use. Talks to an already-running backend.
import { createKiloClient } from "@kilocode/sdk/v2"
const port = process.env.PORT!
const pass = process.env.KILO_SERVER_PASSWORD
const headers = pass
? { Authorization: "Basic " + Buffer.from("kilo:" + pass).toString("base64") }
: undefined
const client = createKiloClient({
baseUrl: `http://127.0.0.1:${port}`,
headers,
directory: process.cwd(),
})
const { data: session } = await client.session.create({}, { throwOnError: true })
await client.session.promptAsync({
sessionID: session.id,
parts: [{ type: "text", text: "hello" }],
})
const events = await client.global.event({})
for await (const ev of events.stream) {
const e = ev as { payload: { type: string } }
console.log(e.payload.type)
if (e.payload.type === "session.idle") break
}
```
```bash
PORT="$PORT" KILO_SERVER_PASSWORD="$KILO_SERVER_PASSWORD" bun /tmp/probe.ts
rm /tmp/probe.ts
```
Reminder: this script **connects to** the server you launched in Section 3 — it does not start one. `createKiloServer()` from the SDK would spawn the PATH `kilo` binary (production CLI), which defeats the point of testing local code.
## 9. Pitfalls
- Running `kilo serve` instead of `bun dev serve` runs the installed prod binary, not your edits.
- Missing `x-kilo-directory` (or `?directory=`) returns `400` from `InstanceMiddleware`.
- `curl` without `-N` buffers SSE output — you won't see events until the connection closes.
- Hardcoding port `4096` breaks when a previous run didn't exit cleanly. Parse the log instead.
- `--port` must appear literally in `argv` to override `opencode.json`'s `server.port` (`packages/opencode/src/cli/network.ts:45`).
- `KILO_SERVER_PASSWORD` must be set **before** launch — changing it after doesn't rotate credentials.
- When sharing `/tmp/kilo-serve.log` / `.pid` across terminals, unique-suffix the paths to avoid clobbering parallel runs.
## 10. After changing server routes
Regenerate the SDK and OpenAPI spec so `/doc` and typed clients stay in sync:
```bash
./script/generate.ts # from repo root
```
See `AGENTS.md` for the full rationale.
+38 -30
View File
@@ -32,7 +32,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "7.2.21",
"version": "7.2.25",
"dependencies": {
"@kilocode/kilo-i18n": "workspace:*",
"@kilocode/kilo-ui": "workspace:*",
@@ -88,7 +88,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "7.2.21",
"version": "7.2.25",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -121,7 +121,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "7.2.21",
"version": "7.2.25",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -172,7 +172,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
"version": "7.2.21",
"version": "7.2.25",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -201,7 +201,7 @@
},
"packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway",
"version": "7.2.21",
"version": "7.2.25",
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/anthropic": "3.0.71",
@@ -210,7 +210,7 @@
"@clack/prompts": "1.0.0-alpha.1",
"@kilocode/plugin": "workspace:*",
"@kilocode/sdk": "workspace:*",
"@openrouter/ai-sdk-provider": "2.5.1",
"@openrouter/ai-sdk-provider": "2.8.1",
"ai": "catalog:",
"open": "10.1.2",
"zod": "catalog:",
@@ -237,7 +237,7 @@
},
"packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n",
"version": "7.2.21",
"version": "7.2.25",
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
@@ -250,7 +250,7 @@
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
"version": "7.2.21",
"version": "7.2.25",
"dependencies": {
"@kilocode/kilo-gateway": "workspace:*",
"@opentelemetry/api": "1.9.0",
@@ -270,7 +270,7 @@
},
"packages/kilo-ui": {
"name": "@kilocode/kilo-ui",
"version": "7.2.21",
"version": "7.2.25",
"dependencies": {
"@kobalte/core": "0.13.11",
"@opencode-ai/shared": "workspace:*",
@@ -305,7 +305,7 @@
},
"packages/kilo-vscode": {
"name": "kilo-code",
"version": "7.2.21",
"version": "7.2.25",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-i18n": "workspace:*",
@@ -365,7 +365,7 @@
},
"packages/opencode": {
"name": "@kilocode/cli",
"version": "7.2.21",
"version": "7.2.25",
"bin": {
"kilo": "./bin/kilo",
"kilocode": "./bin/kilo",
@@ -381,7 +381,7 @@
"@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.102",
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.63",
"@ai-sdk/google-vertex": "4.0.112",
"@ai-sdk/groq": "3.0.31",
@@ -400,7 +400,7 @@
"@effect/platform-node": "catalog:",
"@gitlab/gitlab-ai-provider": "3.6.0",
"@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/node-server": "1.19.11",
"@hono/node-server": "1.19.13",
"@hono/node-ws": "1.3.0",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
@@ -416,7 +416,7 @@
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/script": "workspace:*",
"@openrouter/ai-sdk-provider": "2.5.1",
"@openrouter/ai-sdk-provider": "2.8.1",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "2.6.1",
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
@@ -443,7 +443,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.4.2",
"gitlab-ai-provider": "6.6.0",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
@@ -520,7 +520,7 @@
},
"packages/plugin": {
"name": "@kilocode/plugin",
"version": "7.2.21",
"version": "7.2.25",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"effect": "catalog:",
@@ -545,7 +545,7 @@
},
"packages/script": {
"name": "@opencode-ai/script",
"version": "7.2.21",
"version": "7.2.25",
"dependencies": {
"semver": "^7.6.3",
},
@@ -556,7 +556,7 @@
},
"packages/sdk/js": {
"name": "@kilocode/sdk",
"version": "7.2.21",
"version": "7.2.25",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -571,7 +571,7 @@
},
"packages/shared": {
"name": "@opencode-ai/shared",
"version": "7.2.21",
"version": "7.2.25",
"bin": {
"opencode": "./bin/opencode",
},
@@ -595,7 +595,7 @@
},
"packages/storybook": {
"name": "@opencode-ai/storybook",
"version": "7.2.21",
"version": "7.2.25",
"devDependencies": {
"@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:",
@@ -618,7 +618,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "7.2.21",
"version": "7.2.25",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
@@ -722,7 +722,7 @@
"@types/node": "22.13.9",
"@types/semver": "7.7.1",
"@typescript/native-preview": "7.0.0-dev.20260316.1",
"ai": "6.0.158",
"ai": "6.0.168",
"cross-spawn": "7.0.6",
"diff": "8.0.4",
"dompurify": "3.3.3",
@@ -790,7 +790,7 @@
"@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.46", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XRKR0zgRyegdmtK5CDUEjlyRp0Fo+XVCdoG+301U1SGtgRIAYG3ObVtgzVJBVpJdHFSLHuYeLTnNiQoUxD7+FQ=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.102", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-GrwDpaYJiVafrsA1MTbZtXPcQUI67g5AXiJo7Y1F8b+w+SiYHLk3ZIn1YmpQVoVAh2bjvxjj+Vo0AvfskuGH4g=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="],
"@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="],
@@ -1230,7 +1230,7 @@
"@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="],
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
"@hono/node-server": ["@hono/node-server@1.19.13", "", { "peerDependencies": { "hono": "^4" } }, "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ=="],
"@hono/node-ws": ["@hono/node-ws@1.3.0", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.19.2", "hono": "^4.6.0" } }, "sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q=="],
@@ -1576,7 +1576,7 @@
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="],
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
@@ -2440,7 +2440,7 @@
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
@@ -2524,7 +2524,7 @@
"aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="],
"ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="],
"ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="],
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
@@ -3360,7 +3360,7 @@
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
"gitlab-ai-provider": ["gitlab-ai-provider@6.4.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Wyw6uslCuipBOr/NYwAtpgXEUJj68iJY5aekad2DjePN99JetKVQBqkLgAy9PZp2EA4OuscfRQu9qKIBN/evNw=="],
"gitlab-ai-provider": ["gitlab-ai-provider@6.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-jUxYnKA4XQaPc3wxACDZ8bPDXO0Mzx7cZaBDxbT2uGgLqtGZmSi+9tVNIg7louSS+s/ioVra3SoUz3iOFVhKPA=="],
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
@@ -5098,10 +5098,14 @@
"@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
"@modelcontextprotocol/sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
"@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"@morphllm/morphsdk/ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="],
"@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="],
"@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
@@ -5272,8 +5276,6 @@
"@vscode/vsce/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="],
"ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uz8tIlkDgQJG9Js2Wh9JHzd4kI9+hYJqf9XXJLx60vyN5mRIqhr49iwR5zGP5Gl8odp2PeR3Gh2k+5bh3Z1HHw=="],
@@ -5284,6 +5286,8 @@
"ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.75", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-V8UKK4fNpI9cnrtsZBvUp9O9J6Y9fTKBRoSLyEaNGPirACewixmLDbXsSgAeownPVWiWpK34bFysd+XouI5Ywg=="],
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="],
"ajv-keywords/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
"app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
@@ -5826,6 +5830,8 @@
"@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"@morphllm/morphsdk/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
"@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
@@ -6366,6 +6372,8 @@
"@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"@morphllm/morphsdk/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
"@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-PnZh7bJ97lE8BsAiICc653OV468GjehAs9lXV11DKXw=",
"aarch64-linux": "sha256-VMcH747sR0wDCOXXDbnHQQid9WeigWr/HtRrj2yHxuU=",
"aarch64-darwin": "sha256-lsNiZcz7NjGmuls22z2VCbXwsr1h3Q0DlnI46r5RxeA=",
"x86_64-darwin": "sha256-z4yto3ZjuXpKP5vP5ppl2oX7T0EleSQAByyMqPO0uiE="
"x86_64-linux": "sha256-uDu9FY2G8j6AzpXAQ3WoWLJ9uyh5R7xqcAVzij6dpdU=",
"aarch64-linux": "sha256-GtVSse+wWwmFA9e3XkAp+1spPH2u4C2jqJ9HUKQRJ44=",
"aarch64-darwin": "sha256-UrZIY7v59gB0k0+/CIXA/NFrR3FSE+5Lcsz9qZffsqM=",
"x86_64-darwin": "sha256-/thTDanC0f1rczDj7RbgeFyaRfuhMdbRXrgfH6KRejs="
}
}
+2 -2
View File
@@ -52,7 +52,7 @@
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.48",
"ai": "6.0.158",
"ai": "6.0.168",
"cross-spawn": "7.0.6",
"hono": "4.12.12",
"hono-openapi": "1.1.2",
@@ -145,6 +145,6 @@
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
"stream-chat@9.38.0": "patches/stream-chat@9.38.0.patch"
},
"version": "7.2.21",
"version": "7.2.25",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "7.2.21",
"version": "7.2.25",
"description": "",
"type": "module",
"exports": {
+6 -5
View File
@@ -288,11 +288,12 @@ export function Titlebar() {
</div>
</Show>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
{import.meta.env.VITE_KILO_CHANNEL && ["beta", "dev"].includes(import.meta.env.VITE_KILO_CHANNEL) && ( // kilocode_change
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
{import.meta.env.VITE_KILO_CHANNEL.toUpperCase()}
</div>
)}
{import.meta.env.VITE_KILO_CHANNEL &&
["beta", "dev"].includes(import.meta.env.VITE_KILO_CHANNEL) && ( // kilocode_change
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
{import.meta.env.VITE_KILO_CHANNEL.toUpperCase()}
</div>
)}
</div>
</div>
</div>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
"version": "7.2.21",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "7.2.21",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"scripts": {
+49 -49
View File
@@ -2299,6 +2299,53 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "kilo-desktop"
version = "0.0.0"
dependencies = [
"chrono",
"comrak",
"dirs",
"futures",
"gtk",
"listeners",
"objc2 0.6.3",
"objc2-web-kit",
"process-wrap",
"reqwest 0.12.24",
"semver",
"serde",
"serde_json",
"specta",
"specta-typescript",
"tauri",
"tauri-build 2.5.2",
"tauri-plugin-clipboard-manager",
"tauri-plugin-decorum",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-http",
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-os",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-store",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tauri-specta",
"tokio",
"tokio-stream",
"tracing",
"tracing-appender",
"tracing-subscriber",
"uuid",
"webkit2gtk",
"windows-core 0.62.2",
"windows-sys 0.61.2",
]
[[package]]
name = "kuchikiki"
version = "0.8.8-speedreader"
@@ -3093,53 +3140,6 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "kilo-desktop"
version = "0.0.0"
dependencies = [
"chrono",
"comrak",
"dirs",
"futures",
"gtk",
"listeners",
"objc2 0.6.3",
"objc2-web-kit",
"process-wrap",
"reqwest 0.12.24",
"semver",
"serde",
"serde_json",
"specta",
"specta-typescript",
"tauri",
"tauri-build 2.5.2",
"tauri-plugin-clipboard-manager",
"tauri-plugin-decorum",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-http",
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-os",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-store",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tauri-specta",
"tokio",
"tokio-stream",
"tracing",
"tracing-appender",
"tracing-subscriber",
"uuid",
"webkit2gtk",
"windows-core 0.62.2",
"windows-sys 0.61.2",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -4163,9 +4163,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.103.8"
version = "0.103.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
dependencies = [
"ring",
"rustls-pki-types",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "7.2.21"
version = "7.2.25"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.21/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.21/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.21/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.21/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.21/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+5
View File
@@ -33,6 +33,11 @@ export const AutomateNav: NavSection[] = [
href: "/automate/extending/shell-integration",
children: "Shell Integration",
},
{
href: "/automate/extending/plugins",
children: "Plugins",
platform: "new",
},
{
href: "/automate/extending/auto-launch",
children: "Auto-launch Configuration",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "7.2.21",
"version": "7.2.25",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
@@ -0,0 +1,483 @@
---
title: "Plugins"
description: "Extend the Kilo CLI with custom hooks, tools, auth providers, and more"
platform: new
---
# Plugins
Plugins extend Kilo by hooking into events, adding custom tools, registering auth or model providers, and customizing runtime behavior. They are TypeScript or JavaScript modules loaded at startup, and work in both the Kilo CLI and the VS Code extension.
## What plugins can do
- **Add custom tools** the model can call (like `read`, `write`, `bash`).
- **Intercept tool calls** to mutate arguments, rewrite output, or block dangerous operations.
- **Subscribe to events** — sessions, messages, permissions, LSP diagnostics, file changes, etc.
- **Register auth providers** — OAuth or API-key flows for model providers.
- **Register model providers** — dynamic model catalogs.
- **Mutate chat parameters or headers** sent to the LLM.
- **Customize compaction** — inject or replace the prompt used when a session is compacted.
- **Inject shell environment variables** for commands executed by the agent or user.
---
## Use a plugin
There are three ways to load plugins.
### From a config file
Add an array of plugin specifiers to your config file:
```json
{
"$schema": "https://app.kilo.ai/config.json",
"plugin": [
"@your-org/your-plugin",
"your-plugin@1.2.3",
["your-plugin", { "apiKey": "{env:MY_API_KEY}" }],
"./plugins/local.ts",
"file:///abs/path/plugin.ts"
]
}
```
Each entry can be:
| Form | Loaded from |
| -------------------------------------- | ---------------------------------------------------------------- |
| `"package-name"` | Latest version from npm |
| `"package-name@1.2.3"` | Pinned version from npm |
| `["package-name", { options }]` | npm package with options passed to the plugin function |
| `"./path/plugin.ts"` / `"file:///..."` | Local file (relative to the config file or absolute `file:` URL) |
Config files live in the same locations as the rest of your CLI configuration — see the [CLI configuration reference](/docs/code-with-ai/platforms/cli#configuration).
### From a plugin directory
Drop TypeScript or JavaScript files into a `plugin/` or `plugins/` folder inside any config directory:
- Global: `~/.config/kilo/plugin/`
- Project: `.kilo/plugin/`, `.kilocode/plugin/`, or `.opencode/plugin/`
Every `.ts` or `.js` file in those directories is auto-registered at startup — no need to list them in the config file.
```text
my-project/
├── kilo.json
└── .kilo/
└── plugin/
├── env-guard.ts
└── notifications.ts
```
### From the `kilo plugin` command
Install an npm plugin and patch your config in one step:
```bash
# Install into the current project's config
kilo plugin my-plugin
# Install into your global config
kilo plugin my-plugin --global
# Replace an existing entry
kilo plugin my-plugin --force
```
The command resolves the package, reads its `package.json` for plugin entrypoints, and writes the entry into the appropriate config file (currently `.opencode/opencode.jsonc` / `.opencode/tui.jsonc` for local installs, or `~/.config/kilo/opencode.jsonc` / `~/.config/kilo/tui.jsonc` for `--global`) while preserving JSONC comments.
### How plugins are installed
- **npm plugins** are installed automatically at startup using Bun. Packages and their dependencies are cached in Kilo's XDG cache directory (`~/.cache/kilo/` on Linux, `~/Library/Caches/kilo/` on macOS, `%LOCALAPPDATA%\kilo\` on Windows).
- **Local plugins** are loaded directly from the plugin directory. If your plugin imports external packages, add a `package.json` to your config directory (see [Dependencies](#dependencies)) — Kilo runs `bun install` on startup so imports resolve.
### Load order
Plugins from all sources run on every session. They load in this order:
1. Internal built-ins (Kilo Gateway auth, Codex auth, Copilot auth, Cloudflare, etc.)
2. Global config plugin array (`~/.config/kilo/kilo.json`)
3. Global plugin directory (`~/.config/kilo/plugin/`)
4. Project config plugin array (`kilo.json` / `opencode.json`)
5. Project plugin directory (`.kilo/plugin/` and friends)
Duplicates (same package, same version) are deduplicated. Hooks from multiple plugins run sequentially in load order.
### Disabling external plugins
Set the `KILO_PURE=1` environment variable to skip all external plugins — only built-in plugins will load. Useful for reproducible CI runs or debugging.
---
## Create a plugin
A plugin is a module that exports a function returning a set of [hooks](#hooks-reference).
### Basic structure
Create a file in your plugin directory:
```ts
// .kilo/plugin/hello.ts
import type { Plugin } from "@kilocode/plugin"
const hello: Plugin = async ({ project, client, $, directory, worktree }) => {
console.log("hello plugin loaded")
return {
// hook implementations go here
}
}
export default { id: "hello", server: hello }
```
The plugin function receives a context object:
| Field | Description |
| ------------------------ | ----------------------------------------------------------------- |
| `project` | Current project metadata. |
| `directory` | Current working directory for this session. |
| `worktree` | Git worktree root for this session. |
| `client` | A Kilo SDK client (`@kilocode/sdk`) for calling the local server. |
| `$` | [Bun's shell API](https://bun.com/docs/runtime/shell). |
| `serverUrl` | URL of the local Kilo server. |
| `experimental_workspace` | Register workspace adaptors (used by Agent Manager). |
The function returns a `Hooks` object. Any second argument is the options object passed via config (e.g. the `{ apiKey: "..." }` from `["my-plugin", { apiKey: "..." }]`).
### Module shape
Plugins must default-export a module descriptor. `id` is required for local-file plugins and inferred from `package.json#name` for npm plugins.
```ts
import type { Plugin } from "@kilocode/plugin"
const server: Plugin = async (ctx) => ({
/* hooks */
})
export default {
id: "my-plugin",
server,
}
```
An npm plugin can also expose a TUI entry point (`tui`) for [TUI plugins](#tui-plugins), but `server` and `tui` are separate modules.
### TypeScript support
Install the plugin package locally and import its types:
```bash
bun add -d @kilocode/plugin
```
```ts
import type { Plugin } from "@kilocode/plugin"
import { tool } from "@kilocode/plugin/tool"
```
Kilo automatically creates a `package.json` in config directories that contain a `plugin/` folder and installs `@kilocode/plugin` so types resolve out of the box.
### Engine compatibility
Declare a CLI version range to prevent a plugin from loading against an incompatible build:
```json
{
"name": "my-plugin",
"engines": { "opencode": "^7.0.0" }
}
```
If the running CLI does not satisfy the range, the plugin is skipped and a warning is surfaced.
### Dependencies
Local plugins and custom tools can use external npm packages. Add a `package.json` to your config directory:
```json
// .kilo/package.json
{
"dependencies": {
"shescape": "^2.1.0"
}
}
```
Kilo runs `bun install` at startup so your plugins can import the packages:
```ts
// .kilo/plugin/escape-bash.ts
import { escape } from "shescape"
import type { Plugin } from "@kilocode/plugin"
const EscapeBash: Plugin = async () => ({
"tool.execute.before": async (input, output) => {
if (input.tool === "bash") {
output.args.command = escape(output.args.command)
}
},
})
export default { id: "escape-bash", server: EscapeBash }
```
---
## Hooks reference
Every hook is optional. Return only the ones you care about.
### Lifecycle
| Hook | Description |
| -------- | --------------------------------------------------------------------------------- |
| `config` | Receives the fully-resolved config at startup. Read-only — useful for inspection. |
| `event` | Called for **every** event on the internal bus (see [Events](#events)). |
### Tools
| Hook | Description |
| --------------------- | ----------------------------------------------------------------------------------------------- |
| `tool` | Map of tool name → [tool definition](#custom-tools). Added tools are callable by the model. |
| `tool.execute.before` | Fires before a tool runs; you can mutate `output.args`. |
| `tool.execute.after` | Fires after a tool returns; you can rewrite `output.title`, `output.output`, `output.metadata`. |
| `tool.definition` | Mutate a tool's `description` and `parameters` before they are sent to the model. |
### Chat
| Hook | Description |
| ------------------------ | ---------------------------------------------------------------------------- |
| `chat.message` | Fires when a new user message arrives. Inspect or modify `parts`. |
| `chat.params` | Mutate `temperature`, `topP`, `topK`, `maxOutputTokens`, provider `options`. |
| `chat.headers` | Add or replace HTTP headers on the LLM API call. |
| `permission.ask` | Auto-allow or auto-deny permission prompts. |
| `command.execute.before` | Intercept slash command execution; mutate the resulting `parts`. |
| `shell.env` | Inject environment variables into every shell command Kilo runs. |
### Providers & auth
| Hook | Description |
| ---------- | ------------------------------------------------------------------------------------ |
| `auth` | Register an auth method (OAuth or API key) for a provider, with interactive prompts. |
| `provider` | Dynamically supply a model catalog for a provider (useful for BYO-model gateways). |
### Experimental
These hooks live behind the `experimental.` prefix and may change between releases.
| Hook | Description |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `experimental.chat.messages.transform` | Rewrite the full message history before it is sent to the model. |
| `experimental.chat.system.transform` | Modify the system prompt array. |
| `experimental.session.compacting` | Inject extra context (`output.context`) or replace the compaction prompt entirely (`output.prompt`). |
| `experimental.compaction.autocontinue` | Disable the synthetic "continue" turn that follows compaction. |
| `experimental.text.complete` | Post-process final text parts (e.g. append signatures, redact secrets). |
### Events
The `event` hook fires for every event on Kilo's internal bus. Common event types include:
- **Session**: `session.created`, `session.updated`, `session.idle`, `session.error`, `session.deleted`, `session.compacted`, `session.diff`, `session.status`
- **Message**: `message.updated`, `message.removed`, `message.part.updated`, `message.part.removed`
- **Tool**: `tool.execute.before`, `tool.execute.after`
- **Permission**: `permission.asked`, `permission.replied`
- **File**: `file.edited`, `file.watcher.updated`
- **Shell**: `shell.env`
- **Command**: `command.executed`
- **LSP**: `lsp.updated`, `lsp.client.diagnostics`
- **Todo**: `todo.updated`
- **Server**: `server.connected`
- **Installation**: `installation.updated`
```ts
const server: Plugin = async () => ({
event: async ({ event }) => {
if (event.type === "session.idle") {
// session finished responding
}
},
})
```
---
## Custom tools
Plugins can register tools the model can call alongside the built-in ones. Use the `tool()` helper for type-safety:
```ts
// .kilo/plugin/database.ts
import type { Plugin } from "@kilocode/plugin"
import { tool } from "@kilocode/plugin/tool"
const DatabasePlugin: Plugin = async () => ({
tool: {
query: tool({
description: "Run a read-only SQL query against the project database",
args: {
sql: tool.schema.string().describe("SQL query to execute"),
},
async execute(args, context) {
const { directory, worktree } = context
// your query logic here
return `ran: ${args.sql}`
},
}),
},
})
export default { id: "database", server: DatabasePlugin }
```
`args` uses a [Zod](https://zod.dev) schema via `tool.schema`. The tool's `execute` function receives:
- `args` — validated against your schema
- `context``{ sessionID, messageID, agent, directory, worktree, abort, metadata, ask }`
### Name precedence
If a custom tool uses the same name as a built-in tool, **the custom tool wins**. Prefer unique names unless you intentionally want to override a built-in (for example, to wrap `bash` with extra validation).
### Alternative: standalone tool files
For tools that don't need the full plugin context, drop them in a `tool/` or `tools/` folder inside any config directory — for example `.kilo/tool/database.ts` or `~/.config/kilo/tool/database.ts`. The filename becomes the tool name, and each file exports a `tool()` definition directly. The layout is identical to the [OpenCode custom tools guide](https://opencode.ai/docs/custom-tools); substitute `.kilo/` (or `.kilocode/` / `.opencode/`) for `.opencode/`.
---
## Examples
### Send a notification when a session finishes
```ts
// .kilo/plugin/notify.ts
import type { Plugin } from "@kilocode/plugin"
const Notify: Plugin = async ({ $ }) => ({
event: async ({ event }) => {
if (event.type === "session.idle") {
await $`osascript -e 'display notification "Session complete!" with title "Kilo"'`
}
},
})
export default { id: "notify", server: Notify }
```
{% callout type="tip" %}
The VS Code extension already emits system notifications when a session finishes or errors — this plugin is for the raw CLI / TUI.
{% /callout %}
### Block reads of `.env` files
```ts
// .kilo/plugin/env-guard.ts
import type { Plugin } from "@kilocode/plugin"
const EnvGuard: Plugin = async () => ({
"tool.execute.before": async (input, output) => {
if (input.tool === "read" && String(output.args.filePath).includes(".env")) {
throw new Error("reading .env files is blocked")
}
},
})
export default { id: "env-guard", server: EnvGuard }
```
### Inject environment variables into every shell command
```ts
// .kilo/plugin/inject-env.ts
import type { Plugin } from "@kilocode/plugin"
const InjectEnv: Plugin = async () => ({
"shell.env": async (input, output) => {
output.env.MY_API_KEY = "secret"
output.env.PROJECT_ROOT = input.cwd
},
})
export default { id: "inject-env", server: InjectEnv }
```
### Structured logging
Prefer `client.app.log()` over `console.log` so entries land in Kilo's log pipeline:
```ts
import type { Plugin } from "@kilocode/plugin"
const Logger: Plugin = async ({ client }) => {
await client.app.log({
body: {
service: "my-plugin",
level: "info",
message: "plugin initialized",
extra: { version: "1.0.0" },
},
})
return {}
}
export default { id: "logger", server: Logger }
```
Levels: `debug`, `info`, `warn`, `error`.
### Inject context during session compaction
```ts
// .kilo/plugin/compaction.ts
import type { Plugin } from "@kilocode/plugin"
const Compaction: Plugin = async () => ({
"experimental.session.compacting": async (input, output) => {
output.context.push(
"## Persist across compaction\n- current task status\n- files being actively edited\n- key decisions",
)
},
})
export default { id: "compaction", server: Compaction }
```
Set `output.prompt` to replace the default compaction prompt entirely — when present, `output.context` is ignored.
---
## TUI plugins
Plugins can also target the Kilo TUI itself — registering slash commands, routes, sidebar slots, dialogs, and keybinds. TUI plugins are SolidJS modules exported from `"./tui"` in your plugin package.
TUI plugins live in a separate module namespace (`@kilocode/plugin/tui`) and have their own API surface (`TuiPluginApi`). Because the TUI API is larger and still evolving, this guide doesn't cover it exhaustively — use the types in `@kilocode/plugin/tui` as the reference, and look at the built-in TUI plugins under `packages/opencode/src/cli/cmd/tui/feature-plugins/` for working examples.
---
## Troubleshooting
- **Plugin failed to load** — check the CLI logs with `kilo --print-logs --log-level DEBUG`. Load failures are also surfaced as session errors in the TUI and VS Code extension.
- **Plugin loaded but hooks never fire** — make sure the default export includes `server`:
```ts
export default { id: "my-plugin", server }
```
Named function exports are also accepted for backwards compatibility but should be considered legacy.
- **Local plugin can't find an npm import** — add a `package.json` in the config directory so `bun install` picks up the dependency (see [Dependencies](#dependencies)).
- **Plugin loads in dev but not in CI** — verify `KILO_PURE` is not set, and that npm-installed plugins are cached (Kilo's XDG cache directory — `~/.cache/kilo/` on Linux, `~/Library/Caches/kilo/` on macOS, `%LOCALAPPDATA%\kilo\` on Windows). Run with `--log-level DEBUG` to see install output.
- **Reset the plugin cache** — delete the `node_modules/` under Kilo's cache directory (or the `node_modules` cache under your config directory) and restart Kilo.
---
## Reference
- Types: [`@kilocode/plugin`](https://github.com/Kilo-Org/kilocode/tree/main/packages/plugin) — `Plugin`, `Hooks`, `PluginInput`, `ToolDefinition`, `AuthHook`, `ProviderHook`.
- Example plugin: [`packages/plugin/src/example.ts`](https://github.com/Kilo-Org/kilocode/blob/main/packages/plugin/src/example.ts)
- CLI command: [`kilo plugin`](/docs/code-with-ai/platforms/cli-reference#kilo-plugin)
- Upstream docs (behavior is identical to OpenCode): [opencode.ai/docs/plugins](https://opencode.ai/docs/plugins) and [opencode.ai/docs/custom-tools](https://opencode.ai/docs/custom-tools)
@@ -76,6 +76,61 @@ Check [our provider docs](/docs/ai-providers) for specific context limits on eac
**Recover from context limit errors:** If you hit the `input length and max tokens exceed context limit` error, you can recover by deleting a message, rolling back to a previous checkpoint, or switching over to a model with a long context window like Gemini for a message.
{% /callout %}
## Models During Delegation
When an agent delegates work to a subagent (via the `task` tool), the subagent **inherits the parent agent's model** by default. You can override this per subagent in your config:
{% tabs %}
{% tab label="CLI" %}
```json
{
"agent": {
"explore": {
"model": "anthropic/claude-haiku-4-20250514"
}
}
}
```
This sets the `explore` subagent to always use Haiku regardless of the parent's model. Any subagent without a `model` override uses whatever model the invoking agent is running.
{% /tab %}
{% tab label="VSCode" %}
Subagents inherit the model currently active in the primary agent session — the model shown in the selector at the bottom of the chat. To bypass inheritance and pin a specific model for a subagent:
- **Via Settings** — open **Settings → Models → Model per Mode**, find the subagent, and pick its model.
- **Via config file** — edit `kilo.jsonc`:
```json
{
"agent": {
"explore": {
"model": "anthropic/claude-haiku-4-5"
}
}
}
```
The Settings UI writes the same `agent.<name>.model` entry, so either method produces the same override. Subagents without an explicit model continue to inherit whatever the invoking agent is running.
{% /tab %}
{% tab label="VSCode (Legacy)" %}
In the legacy extension, each mode has **Sticky Models** — switching from one mode to another (e.g., Code → Architect) uses whatever model you last selected for that mode, not the model from the mode you came from. This means you can assign different models to different modes:
- **Architect:** a reasoning-heavy model (Gemini Pro, Claude Opus)
- **Code:** a fast coding model (Claude Sonnet, GPT-4.1)
- **Debug:** a cost-efficient model (Gemini Flash, DeepSeek)
The model selection is remembered per mode across sessions.
{% /tab %}
{% /tabs %}
For details on configuring subagent models, see [Custom Subagents](/docs/customize/custom-subagents).
## Stay Current
The AI model space moves fast. Bookmark [kilo.ai/models](https://kilo.ai/models) and check back when you're evaluating options. What's best today might not be best next month — and that's actually exciting.
@@ -232,11 +232,27 @@ If you have existing `.kilocodemodes` or `custom_modes.yaml` files from the VSCo
Default legacy mode slugs (`code`, `build`, `architect`, `ask`, `debug`, `orchestrator`) are skipped during migration since they map to built-in agents (`build``code`, `architect``plan`).
### Legacy File Locations
The current VSCode extension reads the legacy `custom_modes.yaml` file from its own global storage directory. Helpful for inspecting or fixing the file before the one-time migration runs:
| OS | Path |
| ------- | ----------------------------------------------------------------------------------------------------- |
| macOS | `~/Library/Application Support/Code/User/globalStorage/kilocode.kilo-code/settings/custom_modes.yaml` |
| Linux | `~/.config/Code/User/globalStorage/kilocode.kilo-code/settings/custom_modes.yaml` |
| Windows | `%APPDATA%\Code\User\globalStorage\kilocode.kilo-code\settings\custom_modes.yaml` |
Project-level `.kilocodemodes` and workspace-scoped files are handled by the CLI backend that the extension delegates to — see the [CLI tab](#cli) for the full load-order table. After the extension migrates on startup, the legacy file is no longer consulted; remove new modes through the extension UI instead of editing `custom_modes.yaml` directly.
{% /tab %}
{% tab label="CLI" %}
In the CLI, custom behavioral profiles are called **agents** instead of modes. Agents are defined as Markdown files with YAML frontmatter or as entries in the `agent` key of your config file.
{% callout type="warning" %}
**Legacy `custom_modes.yaml` is not loaded from `~/.config/kilo/`.** If you're migrating from the legacy VSCode extension, global custom modes are read from `~/.kilocode/cli/global/settings/custom_modes.yaml` (not from the CLI's XDG config directory). The recommended approach is to convert legacy modes to agent `.md` files and place them in `~/.config/kilo/agent/` instead — see [Markdown files](#3-markdown-files-with-yaml-frontmatter) and [Migration](#migration-from-vscode-extension-modes) below.
{% /callout %}
## What's Included in a Custom Agent?
| Property | Description |
@@ -452,6 +468,21 @@ If you have existing `.kilocodemodes` or `custom_modes.yaml` files from the VSCo
Default legacy mode slugs (`code`, `build`, `architect`, `ask`, `debug`, `orchestrator`) are skipped during migration since they map to built-in agents (`build``code`, `architect``plan`).
### Legacy File Lookup Paths
The CLI reads legacy mode files from the following locations (in load order). When the same slug appears in multiple sources, the **last loaded source wins**:
| Load Order | Path | Format | Scope |
|------------|------|--------|-------|
| 1 | VSCode extension global storage `/settings/custom_modes.yaml` | YAML | Global |
| 2 | `~/.kilocode/cli/global/settings/custom_modes.yaml` | YAML | Global |
| 3 | `~/.kilocodemodes` | YAML | Global |
| 4 | `<project>/.kilocodemodes` | YAML | Project (wins on conflict) |
{% callout type="info" %}
`~/.config/kilo/` is the XDG config directory for the new agent format — legacy `custom_modes.yaml` placed there will **not** be loaded. Use `~/.config/kilo/agent/*.md` or `~/.config/kilo/kilo.jsonc` for new agent definitions instead.
{% /callout %}
{% /tab %}
{% tab label="VSCode (Legacy)" %}
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4524d1fe8b44cf2cce15bdf3ad100572b57fa479fefe456f1c141ebca8d5187d
size 28078
oid sha256:16e602e6a870e4b66a466a0895f92e9df117f6137809c79ed797f24d2b6a1e88
size 31028
+4 -1
View File
@@ -1,7 +1,7 @@
# Source Code Links
<!-- Auto-generated by script/extract-source-links.ts — DO NOT EDIT -->
<!-- 80 unique URLs extracted from extension and CLI source -->
<!-- 81 unique URLs extracted from extension and CLI source -->
- <https://api.apertis.ai/v1>
<!-- packages/opencode/src/provider/model-cache.ts -->
@@ -16,6 +16,7 @@
<!-- packages/opencode/src/kilocode/components/dialog-claw-setup.tsx -->
<!-- packages/opencode/src/kilocode/components/dialog-claw-upgrade.tsx -->
- <https://app.kilo.ai/config.json>
<!-- packages/kilo-vscode/src/kilo-provider/config-file.ts -->
<!-- packages/opencode/src/config/config.ts -->
- <https://app.kilo.ai/credits>
<!-- packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts -->
@@ -124,6 +125,8 @@
<!-- packages/kilo-vscode/webview-ui/src/i18n/en.ts -->
- <https://openrouter.ai/docs/use-cases/usage-accounting>
<!-- packages/opencode/src/kilocode/session/index.ts -->
- <https://opentelemetry.io/blog/2025/how-to-name-your-span-attributes/>
<!-- packages/opencode/src/server/routes/instance/trace.ts -->
- <https://opncd.ai>
<!-- packages/opencode/src/share/share-next.ts -->
- <https://platform.openai.com/docs/api-reference/responses/create>
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "7.2.21",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
@@ -33,7 +33,7 @@
"@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/openai": "3.0.48",
"@ai-sdk/openai-compatible": "2.0.37",
"@openrouter/ai-sdk-provider": "2.5.1",
"@openrouter/ai-sdk-provider": "2.8.1",
"@clack/prompts": "1.0.0-alpha.1",
"ai": "catalog:",
"open": "10.1.2",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "7.2.21",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
"version": "7.2.21",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
"version": "7.2.21",
"version": "7.2.25",
"type": "module",
"license": "MIT",
"exports": {
+16
View File
@@ -1,5 +1,21 @@
# kilo-code
## 7.2.25
## 7.2.23
### Patch Changes
- [#9423](https://github.com/Kilo-Org/kilocode/pull/9423) [`a87a461`](https://github.com/Kilo-Org/kilocode/commit/a87a461e971864377d2139c35e778cc98d5cca19) - Fix two Agent Manager tab-bar regressions. New session and terminal tabs now consistently open at the right end of the tab bar instead of sometimes slipping in front of existing tabs. Dragging a terminal tab now shows the same floating label preview as dragging a session tab.
- [#9026](https://github.com/Kilo-Org/kilocode/pull/9026) [`61516b4`](https://github.com/Kilo-Org/kilocode/commit/61516b4d3f5a87f623a34b9112b8e1ff3725f93c) - Support attaching Git changes from prompt mentions in the VS Code extension.
## 7.2.22
### Patch Changes
- [#9444](https://github.com/Kilo-Org/kilocode/pull/9444) [`35f13cb`](https://github.com/Kilo-Org/kilocode/commit/35f13cbc8678f80cf6870c233f74f47b028bf705) - Fix restoring and paginating very long VS Code sessions.
## 7.2.21
### Minor Changes
+10 -1
View File
@@ -2,7 +2,7 @@
"name": "kilo-code",
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
"version": "7.2.21",
"version": "7.2.25",
"icon": "assets/icons/logo-outline-black.png",
"galleryBanner": {
"color": "#FFFFFF",
@@ -61,6 +61,15 @@
}
}
],
"icons": {
"kilo-logo": {
"description": "Kilo Code logo",
"default": {
"fontPath": "assets/icons/kilo-icon-font.woff2",
"fontCharacter": "\\F101"
}
}
},
"viewsContainers": {
"activitybar": [
{
+15 -12
View File
@@ -49,6 +49,8 @@ import { handleContinueInWorktree } from "./kilo-provider/continue-worktree"
import { parseMessageFiles, type MessageFile } from "./kilo-provider/message-files"
import { handleFileSearch } from "./kilo-provider/file-search"
import { getTerminalContents } from "./services/terminal/context"
import { disposeGitChangesTarget } from "./kilo-provider/git-changes-target"
import { interceptMessage } from "./kilo-provider/git-changes-request"
import { matchFollowup, recordFollowup, type Followup } from "./kilo-provider/followup-session"
import { clearCommandsCache, loadCommands } from "./kilo-provider/commands"
import { fetchMessagePage, MESSAGE_PAGE_LIMIT } from "./kilo-provider/message-page"
@@ -62,6 +64,7 @@ import {
} from "./services/autocomplete/settings"
import * as ModelState from "./kilo-provider/model-state"
import { handleForkSession } from "./kilo-provider/fork-session"
import { openConfig } from "./kilo-provider/open-config"
import { retryable, backoff, MAX_RETRIES } from "./util/retry"
import { hasGit } from "./kilo-provider/git-status"
// legacy-migration start
@@ -565,17 +568,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.autocompleteConfigDisposable?.dispose()
this.autocompleteConfigDisposable = watchAutocompleteConfig((msg) => this.postMessage(msg))
this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => {
// Run interceptor if attached (e.g., AgentManagerProvider worktree logic)
if (this.onBeforeMessage) {
try {
const result = await this.onBeforeMessage(message)
if (result === null) return // consumed by interceptor
message = result
} catch (error) {
console.error("[Kilo New] KiloProvider: interceptor error:", error)
return
}
}
const intercepted = await interceptMessage(message, {
workspaceDir: (sid) => this.getWorkspaceDirectory(sid ?? this.currentSession?.id),
post: (m) => this.postMessage(m),
error: getErrorMessage,
before: this.onBeforeMessage,
})
if (intercepted === null) return
message = intercepted
await routeSuggestionWebviewMessage(this.questionCtx, message)
if (await ModelState.handleMessage(message.type, message, this.client, (msg) => this.postMessage(msg))) return
@@ -698,6 +698,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "openVSCodeSettings":
vscode.commands.executeCommand("workbench.action.openSettings", message.query)
break
case "openConfigFile":
await openConfig(message.scope, message.labels, this.getProjectDirectory(this.currentSession?.id))
break
case "openMarketplacePanel":
vscode.commands.executeCommand("kilo-code.new.marketplaceButtonClicked", this.projectDirectory)
break
@@ -3322,6 +3325,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.sessionStatusMap.clear()
this.ignoreController?.dispose()
this.chatAutocomplete?.dispose()
this.marketplace?.dispose()
;(this.marketplace?.dispose(), disposeGitChangesTarget())
}
}
@@ -3,9 +3,10 @@ import * as path from "path"
import type { KiloClient, Session } from "@kilocode/sdk/v2/client"
import type { KiloConnectionService } from "../services/cli-backend"
import { getErrorMessage } from "../kilo-provider-utils"
import { resolveLocalDiffTarget } from "../review-utils"
import { isAbsolutePath } from "../path-utils"
import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager"
import { WorktreeStateManager } from "./WorktreeStateManager"
import { remoteRef, WorktreeStateManager } from "./WorktreeStateManager"
import { handleSection } from "./section-handler"
import { chooseBaseBranch, normalizeBaseBranch } from "./base-branch"
import { GitStatsPoller, type LocalStats, type WorktreePresenceResult, type WorktreeStats } from "./GitStatsPoller"
@@ -281,6 +282,7 @@ export class AgentManagerProvider implements Disposable {
if (msg.type === "requestFileSearch" && typeof msg.sessionID !== "string" && this.activeSessionId) {
return { ...msg, sessionID: this.activeSessionId }
}
msg = await this.contextMessage(msg)
const m = msg as unknown as AgentManagerInMessage
const worktree = await this.onWorktreeMessage(m)
@@ -302,6 +304,36 @@ export class AgentManagerProvider implements Disposable {
return msg
}
private async contextMessage(msg: Record<string, unknown>): Promise<Record<string, unknown>> {
if (msg.type !== "requestGitChangesContext") return msg
const ctx = typeof msg.agentManagerContext === "string" ? msg.agentManagerContext : undefined
const target = ctx ? await this.contextTarget(ctx) : undefined
const sid = typeof msg.sessionID === "string" ? msg.sessionID : this.activeSessionId
const next = sid && typeof msg.sessionID !== "string" ? { ...msg, sessionID: sid } : msg
if (target) return { ...next, ...target }
if (!sid) return next
const state = this.getStateManager()
const session = state?.getSession(sid)
const worktree = session?.worktreeId ? state?.getWorktree(session.worktreeId) : undefined
if (!worktree) return next
return { ...next, contextDirectory: worktree.path, gitChangesBase: remoteRef(worktree) }
}
private async contextTarget(ctx: string): Promise<Record<string, unknown> | undefined> {
if (ctx === "local") {
const root = this.getRoot()
if (!root) return undefined
const target = await resolveLocalDiffTarget(this.gitOps, (...args) => this.log(...args), root)
if (!target) return { contextDirectory: root }
return { contextDirectory: target.directory, gitChangesBase: target.baseBranch }
}
const worktree = this.getStateManager()?.getWorktree(ctx)
if (!worktree) return undefined
return { contextDirectory: worktree.path, gitChangesBase: remoteRef(worktree) }
}
private async onWorktreeMessage(m: AgentManagerInMessage): Promise<Record<string, unknown> | null | undefined> {
if (m.type === "agentManager.createWorktree") return this.onCreateWorktree(m.baseBranch, m.branchName)
if (m.type === "agentManager.deleteWorktree") return this.onDeleteWorktree(m.worktreeId)
@@ -0,0 +1,117 @@
import { existsSync } from "fs"
import * as os from "os"
import * as path from "path"
export type Scope = "global" | "local"
export type Source =
| "sourceXdg"
| "sourceHomeKilo"
| "sourceHomeKilocode"
| "sourceHomeOpencode"
| "sourceEnvFile"
| "sourceEnvDir"
| "sourceEnvContent"
| "sourceProjectKilo"
| "sourceProjectRoot"
| "sourceProjectKilocode"
| "sourceProjectOpencode"
export interface Entry {
file?: string
name: string
source: Source
exists: boolean
loaded: boolean
legacy?: boolean
recommended?: boolean
virtual?: boolean
}
const SCHEMA = "https://app.kilo.ai/config.json"
const MODERN = ["kilo.jsonc", "kilo.json"]
const LEGACY = ["opencode.jsonc", "opencode.json"]
const FILES = [...MODERN, ...LEGACY]
const GLOBAL = ["kilo.jsonc", "kilo.json", "opencode.jsonc", "opencode.json", "config.json"]
const HOME = [".kilo", ".kilocode", ".opencode"]
const SOURCES: Record<string, Source> = {
".kilo": "sourceHomeKilo",
".kilocode": "sourceHomeKilocode",
".opencode": "sourceHomeOpencode",
}
function row(file: string, source: Source, loaded = true, recommended = false): Entry {
const name = path.basename(file)
return {
file,
name,
source,
exists: existsSync(file),
loaded: loaded && existsSync(file),
legacy: name.startsWith("opencode") || name === "config.json" || file.includes(`${path.sep}.kilocode${path.sep}`),
recommended,
}
}
function ensure(list: Entry[], file: string, source: Source) {
if (list.some((item) => item.file === file)) return list
return [...list, row(file, source, true, true)]
}
export function globalFiles() {
const root = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "kilo")
const base = GLOBAL.map((file) => row(path.join(root, file), "sourceXdg")).filter((item) => item.exists)
const dirs = HOME.flatMap((dir) => {
const base = path.join(os.homedir(), dir)
if (!existsSync(base)) return []
return FILES.map((file) => row(path.join(base, file), SOURCES[dir])).filter((item) => item.exists)
})
const env = process.env.KILO_CONFIG ? [row(process.env.KILO_CONFIG, "sourceEnvFile")] : []
const extra = process.env.KILO_CONFIG_DIR
const dir = extra
? ensure(
FILES.map((file) => row(path.join(extra, file), "sourceEnvDir")).filter((item) => item.exists),
path.join(extra, "kilo.jsonc"),
"sourceEnvDir",
)
: []
const virtual: Entry[] = process.env.KILO_CONFIG_CONTENT
? [
{
name: "KILO_CONFIG_CONTENT",
source: "sourceEnvContent",
exists: true,
loaded: true,
virtual: true,
},
]
: []
return ensure([...base, ...dirs, ...env, ...dir, ...virtual], path.join(root, "kilo.jsonc"), "sourceXdg")
}
export function localFiles(root: string) {
const enabled = !process.env.KILO_DISABLE_PROJECT_CONFIG
const dirs = [path.join(root, ".kilo"), root, path.join(root, ".kilocode"), path.join(root, ".opencode")]
const list = dirs.flatMap((dir) => FILES.map((file) => row(path.join(dir, file), localSource(root, dir), enabled)))
return ensure(
list.filter((item) => item.exists),
path.join(root, ".kilo", "kilo.jsonc"),
"sourceProjectKilo",
).map((item) => (enabled ? item : { ...item, loaded: false }))
}
function localSource(root: string, dir: string) {
if (dir === root) return "sourceProjectRoot"
if (dir.endsWith(`${path.sep}.kilo`)) return "sourceProjectKilo"
if (dir.endsWith(`${path.sep}.kilocode`)) return "sourceProjectKilocode"
return "sourceProjectOpencode"
}
export function content() {
return `{
"$schema": "${SCHEMA}"
}
`
}
@@ -0,0 +1,28 @@
import { getGitChangesContext } from "../services/git/context"
type Input = {
requestId: string
dir: string
base?: string
post: (message: unknown) => void
error: (error: unknown) => string
}
export async function captureGitChangesContext(input: Input): Promise<void> {
try {
const output = await getGitChangesContext(input.dir, input.base)
input.post({
type: "gitChangesContextResult",
requestId: input.requestId,
content: output.content,
truncated: output.truncated,
})
} catch (error) {
console.error("[Kilo New] Failed to capture git changes context:", error)
input.post({
type: "gitChangesContextError",
requestId: input.requestId,
error: input.error(error) || "Failed to capture git changes",
})
}
}
@@ -0,0 +1,32 @@
import { captureGitChangesContext } from "./git-changes-context"
import { resolveGitChangesTarget } from "./git-changes-target"
type Interceptor = (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
type Context = {
workspaceDir: (sessionID: string | undefined) => string
post: (message: unknown) => void
error: (error: unknown) => string
before?: Interceptor | null
}
export async function interceptMessage(
msg: Record<string, unknown>,
ctx: Context,
): Promise<Record<string, unknown> | null> {
const next = ctx.before
? await ctx.before(msg).catch((e) => (console.error("[Kilo New] interceptor error:", e), null))
: msg
if (next === null || next.type !== "requestGitChangesContext") return next
const sid = typeof next.sessionID === "string" ? next.sessionID : undefined
const dir = ctx.workspaceDir(sid)
const resolved = await resolveGitChangesTarget(next, dir)
await captureGitChangesContext({
requestId: typeof resolved.requestId === "string" ? resolved.requestId : "",
dir: typeof resolved.contextDirectory === "string" ? resolved.contextDirectory : dir,
base: typeof resolved.gitChangesBase === "string" ? resolved.gitChangesBase : undefined,
post: ctx.post,
error: ctx.error,
}).catch((e) => console.error("[Kilo New] git changes error:", e))
return null
}
@@ -0,0 +1,24 @@
import { GitOps } from "../agent-manager/GitOps"
import { resolveLocalDiffTarget } from "../review-utils"
let shared: GitOps | undefined
function ops(): GitOps {
if (shared && !shared.disposed) return shared
shared = new GitOps({ log: () => undefined })
return shared
}
export function disposeGitChangesTarget(): void {
shared?.dispose()
shared = undefined
}
export async function resolveGitChangesTarget(message: Record<string, unknown>, dir: string) {
if (message.type !== "requestGitChangesContext") return message
if (typeof message.contextDirectory === "string" || typeof message.gitChangesBase === "string") return message
const target = await resolveLocalDiffTarget(ops(), () => undefined, dir)
if (!target) return { ...message, contextDirectory: dir }
return { ...message, contextDirectory: target.directory, gitChangesBase: target.baseBranch }
}
@@ -3,6 +3,9 @@ import { retry } from "../services/cli-backend/retry"
export const MESSAGE_PAGE_LIMIT = 80
// Bound assistant-boundary backfill so corrupt histories cannot load an entire session.
const FILL_LIMIT = 2
/**
* Build the same base64url-encoded cursor format the server emits so a
* synthesized cursor round-trips through `session.messages({ before })`.
@@ -46,12 +49,13 @@ export async function fetchMessagePage(
return { items, cursor }
}
const fill = async (page: Awaited<ReturnType<typeof read>>): Promise<Awaited<ReturnType<typeof read>>> => {
const fill = async (page: Awaited<ReturnType<typeof read>>, depth = 0): Promise<Awaited<ReturnType<typeof read>>> => {
if (page.items[0]?.info.role !== "assistant") return page
if (depth >= FILL_LIMIT) return page
if (!page.cursor || input.signal?.aborted) return page
const next = await read(page.cursor)
const items = [...next.items, ...page.items]
return fill({ items, cursor: next.cursor })
return fill({ items, cursor: next.cursor }, depth + 1)
}
return fill(await read(input.before))
@@ -0,0 +1,72 @@
import * as path from "path"
import * as vscode from "vscode"
import { content, globalFiles, localFiles, type Entry, type Scope, type Source } from "./config-file"
interface Labels extends Record<Source, string> {
scope: string
statusLoaded: string
statusLoadedLegacy: string
statusNotLoaded: string
statusCreate: string
title: string
placeholder: string
noWorkspace: string
openFailed: string
}
export async function openConfig(scope: Scope, labels: Labels, root?: string): Promise<void> {
if (scope === "local" && !root) {
void vscode.window.showWarningMessage(labels.noWorkspace)
return
}
const list = scope === "global" ? globalFiles() : localFiles(root!)
const picked = await pick(list, labels)
if (!picked?.file) return
await open(picked.file, labels)
}
async function pick(list: Entry[], labels: Labels) {
const editable = list.filter((item) => !item.virtual)
if (editable.length === 1) return editable[0]
const picked = await vscode.window.showQuickPick(
editable.map((item) => ({
label: item.recommended && !item.exists ? `$(add) ${item.name}` : `$(json) ${item.name}`,
description: item.exists ? status(item, labels) : labels.statusCreate,
detail: `${labels[item.source]} - ${item.file}`,
item,
})),
{
title: labels.title,
placeHolder: labels.placeholder,
},
)
return picked?.item
}
function status(item: Entry, labels: Labels) {
if (!item.loaded) return labels.statusNotLoaded
if (item.legacy) return labels.statusLoadedLegacy
return labels.statusLoaded
}
async function open(file: string, labels: Labels) {
const uri = vscode.Uri.file(file)
try {
await vscode.workspace.fs.createDirectory(vscode.Uri.file(path.dirname(file)))
const exists = await vscode.workspace.fs.stat(uri).then(
() => true,
() => false,
)
if (!exists) await vscode.workspace.fs.writeFile(uri, Buffer.from(content()))
const doc = await vscode.workspace.openTextDocument(uri)
await vscode.window.showTextDocument(doc, { preview: false })
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
console.error("[Kilo New] Failed to open config file:", file, err)
void vscode.window.showErrorMessage(labels.openFailed.replace("{{message}}", () => msg))
}
}
+21 -1
View File
@@ -17,6 +17,10 @@ import { KILO_AUTO, parseModelString } from "./shared/provider-model"
*/
type AuthState = "api" | "oauth" | "wellknown"
function disabledWithout(list: string[] | undefined, id: string) {
return (list ?? []).filter((item) => item !== id)
}
/** Fetch auth methods alongside the provider list. Auth states default to empty (endpoint not yet available). */
export async function fetchProviderData(client: KiloClient, dir: string) {
const authRequest =
@@ -232,6 +236,9 @@ export async function disconnectProvider(
try {
const globalConfig = (await ctx.client.global.config.get({ throwOnError: true })).data ?? {}
const configured = !!globalConfig.provider?.[id]
const { response } = await fetchProviderData(ctx.client, ctx.workspaceDir)
const active = response.all.find((item) => item.id === id)
const oauth = active?.source === "custom" && configured
// Remove auth store entry. Config-sourced providers may not have an auth
// store entry (credentials come from config or env), so failure is non-fatal.
@@ -251,7 +258,7 @@ export async function disconnectProvider(
// server rebuilds state from config. Add to disabled_providers so the server
// excludes them. The config entry is preserved (user may re-enable later).
// This matches the desktop app's disableProvider() pattern.
if (configured) {
if (configured && !oauth) {
const disabled = globalConfig.disabled_providers ?? []
if (!disabled.includes(id)) {
const merged = (
@@ -267,6 +274,19 @@ export async function disconnectProvider(
}
}
if (oauth) {
const disabled = disabledWithout(globalConfig.disabled_providers, id)
if (disabled.length !== (globalConfig.disabled_providers ?? []).length) {
const merged = (
await ctx.client.global.config.update({ config: { disabled_providers: disabled } }, { throwOnError: true })
).data
if (merged) {
setCachedConfig({ type: "configLoaded", config: merged })
ctx.postMessage({ type: "configUpdated", config: merged })
}
}
}
await ctx.disposeGlobal(`provider disconnect (${id})`)
await ctx.fetchAndSendProviders()
ctx.postMessage({ type: "providerDisconnected", requestId, providerID: id })
@@ -0,0 +1,213 @@
import * as fs from "fs/promises"
import * as path from "path"
import { spawn } from "../../util/process"
const LIMIT = 400_000
const SMALL = 80_000
const TIMEOUT = 15_000
type Result = {
out: string
err: string
code: number | null
signal: NodeJS.Signals | null
truncated: boolean
error?: string
}
export async function getGitChangesContext(
dir: string,
base?: string,
): Promise<{ content: string; truncated: boolean }> {
const probe = await run(["rev-parse", "--is-inside-work-tree"], dir, SMALL)
if (probe.error) return done(dir, `Unable to read git changes: ${probe.error}`)
if (probe.code !== 0 || probe.out.trim() !== "true") return done(dir, "Not a git repository.")
const head = await run(["rev-parse", "--verify", "HEAD"], dir, SMALL)
if (head.error) return done(dir, `Unable to read git changes: ${head.error}`)
if (base && head.code === 0) return await against(dir, base)
return await local(dir, head.code === 0)
}
async function local(dir: string, born: boolean): Promise<{ content: string; truncated: boolean }> {
const [status, diff, untracked] = await Promise.all([
run(["status", "--short"], dir, SMALL),
changes(dir, born),
run(["ls-files", "--others", "--exclude-standard", "-z"], dir, SMALL),
])
const fail = status.error ?? diff.error ?? untracked.error
if (fail) return done(dir, `Unable to read git changes: ${fail}`)
if (status.code !== 0 && !status.truncated) return done(dir, `Unable to read git status:\n${output(status)}`.trim())
if (diff.code !== 0 && !diff.truncated) return done(dir, `Unable to read git diff:\n${output(diff)}`.trim())
if (untracked.code !== 0 && !untracked.truncated)
return done(dir, `Unable to read untracked files:\n${output(untracked)}`.trim())
const extra = await untrackedDiff(dir, untracked.out)
const body = [diff.out.trim(), extra.content.trim()].filter(Boolean).join("\n\n")
const changed = status.out.trim() || body.trim()
if (!changed) return done(dir, "No changes in working directory.")
const truncated = status.truncated || diff.truncated || untracked.truncated || extra.truncated
const note = truncated ? "\n\nOutput truncated." : ""
return cap(
`Working directory: ${dir}\n\nStatus:\n${status.out.trim() || "(empty)"}\n\nDiff:\n${body || "(empty)"}${note}`,
truncated,
)
}
async function against(dir: string, base: string): Promise<{ content: string; truncated: boolean }> {
const ancestor = await run(["merge-base", "HEAD", base], dir, SMALL)
if (ancestor.error) return done(dir, `Unable to resolve git base ${base}: ${ancestor.error}`)
if (ancestor.code !== 0) return done(dir, `Unable to resolve git base ${base}:\n${output(ancestor)}`.trim())
const ref = ancestor.out.trim()
const [status, diff, untracked] = await Promise.all([
run(["diff", "--name-status", "--no-renames", ref], dir, SMALL),
run(["diff", ref], dir, LIMIT),
run(["ls-files", "--others", "--exclude-standard", "-z"], dir, SMALL),
])
const fail = status.error ?? diff.error ?? untracked.error
if (fail) return done(dir, `Unable to read git changes: ${fail}`)
if (status.code !== 0 && !status.truncated)
return done(dir, `Unable to read changed files:\n${output(status)}`.trim())
if (diff.code !== 0 && !diff.truncated) return done(dir, `Unable to read git diff:\n${output(diff)}`.trim())
if (untracked.code !== 0 && !untracked.truncated)
return done(dir, `Unable to read untracked files:\n${output(untracked)}`.trim())
const extra = await untrackedDiff(dir, untracked.out)
const files = [status.out.trim(), listed(untracked.out)].filter(Boolean).join("\n")
const body = [diff.out.trim(), extra.content.trim()].filter(Boolean).join("\n\n")
const changed = files.trim() || body.trim()
if (!changed) return done(dir, `Base: ${base}\n\nNo changes in worktree diff.`)
const truncated = status.truncated || diff.truncated || untracked.truncated || extra.truncated
const note = truncated ? "\n\nOutput truncated." : ""
return cap(
`Working directory: ${dir}\nBase: ${base}\nMerge base: ${ref}\n\nFiles:\n${files || "(empty)"}\n\nDiff:\n${body || "(empty)"}${note}`,
truncated,
)
}
function listed(raw: string): string {
return raw
.split("\0")
.filter(Boolean)
.map((file) => `A\t${file}`)
.join("\n")
}
async function changes(dir: string, born: boolean): Promise<Result> {
if (born) return run(["diff", "HEAD"], dir, LIMIT)
const [cached, work] = await Promise.all([run(["diff", "--cached"], dir, LIMIT), run(["diff"], dir, LIMIT)])
return {
out: [cached.out.trim(), work.out.trim()].filter(Boolean).join("\n\n"),
err: [cached.err.trim(), work.err.trim()].filter(Boolean).join("\n"),
code: cached.code !== 0 ? cached.code : work.code,
signal: cached.signal ?? work.signal,
truncated: cached.truncated || work.truncated,
error: cached.error ?? work.error,
}
}
async function untrackedDiff(dir: string, raw: string): Promise<{ content: string; truncated: boolean }> {
const files = raw.split("\0").filter(Boolean)
const parts: string[] = []
let used = 0
let truncated = false
for (const file of files) {
const full = path.join(dir, file)
const stat = await fs.stat(full).catch(() => undefined)
if (!stat?.isFile()) continue
if (stat.size > LIMIT) {
truncated = true
parts.push(patch(file, `<${stat.size} byte file omitted>`))
continue
}
const buf = await fs.readFile(full).catch(() => undefined)
const next = !buf
? patch(file, `<unreadable file: ${file}>`)
: binary(buf)
? patch(file, `<binary file omitted: ${file}>`)
: patch(file, buf.toString("utf8"))
const size = Buffer.byteLength(next, "utf8") + (parts.length ? 2 : 0)
if (used + size > LIMIT) {
truncated = true
break
}
parts.push(next)
used += size
}
return cap(parts.join("\n\n"), truncated)
}
function binary(buf: Buffer): boolean {
const head = buf.subarray(0, Math.min(buf.length, 8192))
return head.includes(0)
}
function patch(file: string, text: string) {
const header = `diff --git a/${file} b/${file}\nnew file mode 100644\n--- /dev/null\n+++ b/${file}`
if (!text) return header
const lines = text.endsWith("\n") ? text.slice(0, -1).split("\n") : text.split("\n")
const body = lines.map((line) => `+${line}`).join("\n")
return `${header}\n@@ -0,0 +1,${lines.length} @@\n${body}`
}
function cap(content: string, truncated = false) {
if (Buffer.byteLength(content, "utf8") <= LIMIT) return { content, truncated }
const text = Buffer.from(content, "utf8").subarray(0, LIMIT).toString("utf8")
return { content: text, truncated: true }
}
function done(dir: string, text: string) {
return { content: `Working directory: ${dir}\n\n${text}`, truncated: false }
}
function output(result: Result) {
return `${result.err.trim()}${result.err.trim() && result.out.trim() ? "\n" : ""}${result.out.trim()}`
}
function run(args: string[], cwd: string, limit: number): Promise<Result> {
return new Promise((resolve) => {
const state = { out: "", err: "", done: false, truncated: false }
const child = spawn("git", args, { cwd })
const timer = setTimeout(() => {
state.truncated = true
child.kill()
}, TIMEOUT)
const finish = (result: Pick<Result, "code" | "signal" | "error">) => {
if (state.done) return
state.done = true
clearTimeout(timer)
resolve({ out: state.out, err: state.err, truncated: state.truncated, ...result })
}
const collect = (key: "out" | "err", chunk: Buffer) => {
if (state.truncated) return
const used = Buffer.byteLength(state[key], "utf8")
const free = limit - used
if (free <= 0) {
state.truncated = true
child.kill()
return
}
if (chunk.byteLength > free) {
state[key] += chunk.subarray(0, free).toString("utf8")
state.truncated = true
child.kill()
return
}
state[key] += chunk.toString("utf8")
}
child.stdout?.on("data", (chunk: Buffer) => collect("out", chunk))
child.stderr?.on("data", (chunk: Buffer) => collect("err", chunk))
child.on("error", (err) => finish({ code: null, signal: null, error: err.message }))
child.on("close", (code, signal) => finish({ code, signal }))
})
}
@@ -31,7 +31,7 @@ describe("AT_PATTERN", () => {
})
describe("buildMentionResults", () => {
it("includes terminal for empty mention query", () => {
it("includes special mentions for empty mention query", () => {
const result = buildMentionResults("", [])
expect(result[0]).toEqual({
type: "terminal",
@@ -39,6 +39,12 @@ describe("buildMentionResults", () => {
label: "Terminal",
description: "Active terminal output",
})
expect(result[1]).toEqual({
type: "git-changes",
value: "git-changes",
label: "Git changes",
description: "Current session/worktree changes",
})
})
it("includes terminal for matching prefix", () => {
@@ -46,11 +52,21 @@ describe("buildMentionResults", () => {
expect(result.map((item) => item.type)).toEqual(["terminal", "file"])
})
it("omits terminal for unrelated query", () => {
it("includes git changes for matching prefix", () => {
const result = buildMentionResults("git", ["src/git.ts"])
expect(result.map((item) => item.type)).toEqual(["git-changes", "file"])
})
it("omits special mentions for unrelated query", () => {
const result = buildMentionResults("src", ["src/index.ts"])
expect(result.map((item) => item.type)).toEqual(["file"])
})
it("omits git changes when git is unavailable", () => {
const result = buildMentionResults("git", ["src/git.ts"], false)
expect(result.map((item) => item.type)).toEqual(["file"])
})
it("includes folder results", () => {
const result = buildMentionResults("src", [{ path: "src", type: "folder" }])
expect(result).toEqual([{ type: "folder", value: "src" }])
@@ -0,0 +1,130 @@
import { describe, expect, it } from "bun:test"
import * as fs from "fs/promises"
import * as os from "os"
import * as path from "path"
import { getGitChangesContext } from "../../src/services/git/context"
function git(cwd: string, args: string[]) {
const result = Bun.spawnSync({ cmd: ["git", ...args], cwd, stdout: "pipe", stderr: "pipe" })
if (result.exitCode === 0) return Buffer.from(result.stdout).toString("utf8")
throw new Error(Buffer.from(result.stderr).toString("utf8") || Buffer.from(result.stdout).toString("utf8"))
}
async function repo(run: (dir: string) => Promise<void>, commit = true) {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-git-context-"))
try {
git(dir, ["init"])
if (commit)
git(dir, ["-c", "user.name=Kilo", "-c", "user.email=kilo@example.com", "commit", "--allow-empty", "-m", "init"])
await run(dir)
} finally {
await fs.rm(dir, { recursive: true, force: true })
}
}
describe("getGitChangesContext", () => {
it("reports non-git directories", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-no-git-"))
try {
const result = await getGitChangesContext(dir)
expect(result.content).toContain("Not a git repository.")
expect(result.truncated).toBe(false)
} finally {
await fs.rm(dir, { recursive: true, force: true })
}
})
it("reports clean repositories", async () => {
await repo(async (dir) => {
const result = await getGitChangesContext(dir)
expect(result.content).toContain("No changes in working directory.")
expect(result.truncated).toBe(false)
})
})
it("includes tracked diffs", async () => {
await repo(async (dir) => {
await fs.writeFile(path.join(dir, "tracked.txt"), "before\n")
git(dir, ["add", "tracked.txt"])
git(dir, ["-c", "user.name=Kilo", "-c", "user.email=kilo@example.com", "commit", "-m", "tracked"])
await fs.writeFile(path.join(dir, "tracked.txt"), "after\n")
const result = await getGitChangesContext(dir)
expect(result.content).toContain("M tracked.txt")
expect(result.content).toContain("diff --git a/tracked.txt b/tracked.txt")
expect(result.content).toContain("+after")
})
})
it("includes base diffs with patches", async () => {
await repo(async (dir) => {
await fs.writeFile(path.join(dir, "base.txt"), "before\n")
git(dir, ["add", "base.txt"])
git(dir, ["-c", "user.name=Kilo", "-c", "user.email=kilo@example.com", "commit", "-m", "base"])
await fs.writeFile(path.join(dir, "base.txt"), "after\n")
git(dir, ["add", "base.txt"])
git(dir, ["-c", "user.name=Kilo", "-c", "user.email=kilo@example.com", "commit", "-m", "change"])
await fs.writeFile(path.join(dir, "new.txt"), "new\n")
const result = await getGitChangesContext(dir, "HEAD~1")
expect(result.content).toContain("Base: HEAD~1")
expect(result.content).toContain("M\tbase.txt")
expect(result.content).toContain("A\tnew.txt")
expect(result.content).toContain("diff --git a/base.txt b/base.txt")
expect(result.content).toContain("+after")
expect(result.content).toContain("diff --git a/new.txt b/new.txt")
expect(result.content).toContain("+new")
})
})
it("includes untracked file contents as patches", async () => {
await repo(async (dir) => {
await fs.mkdir(path.join(dir, "src"))
await fs.writeFile(path.join(dir, "src", "new.txt"), "hello\nworld\n")
const result = await getGitChangesContext(dir)
expect(result.content).toContain("?? src/")
expect(result.content).toContain("diff --git a/src/new.txt b/src/new.txt")
expect(result.content).toContain("--- /dev/null")
expect(result.content).toContain("+++ b/src/new.txt")
expect(result.content).toContain("+hello")
expect(result.content).toContain("+world")
})
})
it("handles repositories without an initial commit", async () => {
await repo(async (dir) => {
await fs.writeFile(path.join(dir, "new.txt"), "hello\n")
git(dir, ["add", "new.txt"])
const result = await getGitChangesContext(dir)
expect(result.content).not.toContain("Unable to read git diff")
expect(result.content).toContain("A new.txt")
expect(result.content).toContain("diff --git a/new.txt b/new.txt")
expect(result.content).toContain("+hello")
}, false)
})
it("omits binary untracked file contents", async () => {
await repo(async (dir) => {
const bytes = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0xff, 0xfe])
await fs.writeFile(path.join(dir, "blob.bin"), bytes)
const result = await getGitChangesContext(dir)
expect(result.content).toContain("diff --git a/blob.bin b/blob.bin")
expect(result.content).toContain("<binary file omitted: blob.bin>")
expect(result.content).not.toContain("+\u0001")
})
})
it("omits the hunk header for empty untracked files", async () => {
await repo(async (dir) => {
await fs.writeFile(path.join(dir, "empty.txt"), "")
const result = await getGitChangesContext(dir)
expect(result.content).toContain("diff --git a/empty.txt b/empty.txt")
expect(result.content).toContain("new file mode 100644")
expect(result.content).not.toMatch(/empty\.txt[\s\S]*@@ -0,0/)
})
})
})
@@ -14,7 +14,9 @@ import fs from "node:fs"
import path from "node:path"
const ROOT = path.resolve(import.meta.dir, "../..")
const MESSAGES_FILE = path.join(ROOT, "webview-ui/src/types/messages.ts")
const MESSAGES_DIR = path.join(ROOT, "webview-ui/src/types/messages")
const EXTENSION_MESSAGES_FILE = path.join(MESSAGES_DIR, "extension-messages.ts")
const WEBVIEW_MESSAGES_FILE = path.join(MESSAGES_DIR, "webview-messages.ts")
const KILO_PROVIDER_FILE = path.join(ROOT, "src/KiloProvider.ts")
const KILO_PROVIDER_UTILS_FILE = path.join(ROOT, "src/kilo-provider-utils.ts")
// Some wire types (partUpdated, partsUpdated) live in a file shared by the
@@ -25,52 +27,68 @@ function readFile(filePath: string): string {
return fs.readFileSync(filePath, "utf-8")
}
function readMessagesDir(): string {
return fs
.readdirSync(MESSAGES_DIR)
.filter((f) => f.endsWith(".ts"))
.map((f) => readFile(path.join(MESSAGES_DIR, f)))
.join("\n")
}
function readMessageTypeSources(): string {
return readFile(MESSAGES_FILE) + "\n" + readFile(SHARED_STREAM_MESSAGES_FILE)
return readMessagesDir() + "\n" + readFile(SHARED_STREAM_MESSAGES_FILE)
}
/**
* Extract the named union's member names from a single source file.
*
* Reads each ` | MemberName` line after `export type Name =`, skipping
* blank lines and `//` comments, and stops at the first other line.
*/
function extractUnionMembers(src: string, name: string): string[] {
const lines = src.split("\n")
const start = lines.findIndex((l) => new RegExp(`^export type ${name}\\s*=\\s*$`).test(l))
if (start === -1) throw new Error(`Could not find union "${name}"`)
const members: string[] = []
for (const line of lines.slice(start + 1)) {
const m = line.match(/^\s*\|\s*([A-Z]\w+)\b/)
if (m) {
members.push(m[1]!)
continue
}
if (line.trim() === "" || /^\s*\/\//.test(line)) continue
break
}
return members
}
describe("ExtensionMessage type members", () => {
it("all members of ExtensionMessage union are defined as interfaces/types in messages.ts", () => {
const content = readFile(MESSAGES_FILE)
// Extract ExtensionMessage union members
const unionMatch = content.match(
/export type ExtensionMessage\s*=\s*([\s\S]*?)(?=\nexport type|\nexport interface|\nexport function|\n\/\/|$)/,
)
if (!unionMatch) {
expect(false, "Could not find ExtensionMessage union in messages.ts").toBe(true)
return
}
const unionBody = unionMatch[1]!
const memberNames = [...unionBody.matchAll(/\|\s*([A-Z]\w+)\b/g)].map((m) => m[1]!)
it("all members of ExtensionMessage union are defined in message type sources", () => {
const memberNames = extractUnionMembers(readFile(EXTENSION_MESSAGES_FILE), "ExtensionMessage")
const defined = readMessageTypeSources()
const missing = memberNames.filter((name) => {
return !new RegExp(`(interface|type)\\s+${name}\\b`).test(defined)
})
expect(missing, `ExtensionMessage members without definitions: ${missing.join(", ")}`).toEqual([])
expect(
missing,
`ExtensionMessage members without definitions in message type sources: ${missing.join(", ")}`,
).toEqual([])
})
it("all members of WebviewMessage union are defined as interfaces/types in messages.ts", () => {
const content = readFile(MESSAGES_FILE)
const unionMatch = content.match(/export type WebviewMessage\s*=\s*([\s\S]*?)(?=\n\/\/|$)/)
if (!unionMatch) {
expect(false, "Could not find WebviewMessage union in messages.ts").toBe(true)
return
}
const unionBody = unionMatch[1]!
const memberNames = [...unionBody.matchAll(/\|\s*([A-Z]\w+)\b/g)].map((m) => m[1]!)
it("all members of WebviewMessage union are defined in message type sources", () => {
const memberNames = extractUnionMembers(readFile(WEBVIEW_MESSAGES_FILE), "WebviewMessage")
const defined = readMessageTypeSources()
const missing = memberNames.filter((name) => {
return !new RegExp(`(interface|type)\\s+${name}\\b`).test(defined)
})
expect(missing, `WebviewMessage members without definitions: ${missing.join(", ")}`).toEqual([])
expect(
missing,
`WebviewMessage members without definitions in message type sources: ${missing.join(", ")}`,
).toEqual([])
})
})
@@ -109,6 +127,9 @@ describe("mapSSEEventToWebviewMessage output types", () => {
const missing = typeMatches.filter((t) => !typeSet.has(t))
expect(missing, `Types in mapSSEEventToWebviewMessage not in messages.ts: ${missing.join(", ")}`).toEqual([])
expect(
missing,
`Types in mapSSEEventToWebviewMessage not found in message type sources: ${missing.join(", ")}`,
).toEqual([])
})
})
@@ -164,4 +164,34 @@ describe("fetchMessagePage / cursor fallback", () => {
expect(page.items.map((item) => item.info.id)).toEqual(["m1", "m2", "m3", "m4", "m5"])
expect(page.cursor).toBeUndefined()
})
it("bounds assistant turn filling when older pages never reach a user message", async () => {
const { client, calls } = mockClient([
{
items: [message("m5", "assistant", 50), message("m6", "assistant", 60)],
cursor: "c1",
},
{
items: [message("m3", "assistant", 30), message("m4", "assistant", 40)],
cursor: "c2",
},
{
items: [message("m1", "assistant", 10), message("m2", "assistant", 20)],
cursor: "c3",
},
{
items: [message("m0", "user", 0)],
},
])
const page = await fetchMessagePage(client as never, {
sessionID: "s1",
workspaceDir: "/repo",
limit: 2,
})
expect(calls.map((call) => call.before)).toEqual([undefined, "c1", "c2"])
expect(page.items.map((item) => item.info.id)).toEqual(["m1", "m2", "m3", "m4", "m5", "m6"])
expect(page.cursor).toBe("c3")
})
})
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test"
import { fetchProviderData, saveCustomProvider } from "../../src/provider-actions"
import { disconnectProvider, fetchProviderData, saveCustomProvider } from "../../src/provider-actions"
type ExistingGlobal = { disabled_providers?: string[]; provider?: Record<string, unknown> }
@@ -26,6 +26,24 @@ function createCtx(existing: ExistingGlobal = { disabled_providers: [] }) {
return { data: true }
},
},
provider: {
list: async () => ({
data: {
all: [
{
id: "openai",
name: "OpenAI",
source: "custom",
env: [],
models: {},
},
],
connected: ["openai"],
default: {},
},
}),
auth: async () => ({ data: {} }),
},
global: {
config: {
get: async () => ({ data: existing }),
@@ -64,6 +82,26 @@ function createProvider() {
}
}
describe("disconnectProvider", () => {
it("keeps configured provider enabled after disconnecting oauth override", async () => {
const existing = {
disabled_providers: ["openai", "groq"],
provider: {
openai: {
options: { apiKey: "sk-test" },
},
},
}
const { ctx, calls, setCachedConfig } = createCtx(existing)
await disconnectProvider(ctx, "req", "openai", null, setCachedConfig)
expect(calls.remove).toEqual([{ providerID: "openai" }])
expect(calls.config).toEqual([{ config: { disabled_providers: ["groq"] } }])
expect(calls.refresh).toBe(1)
})
})
describe("saveCustomProvider", () => {
it("preserves auth when the api key field is unchanged", async () => {
const { ctx, calls, setCachedConfig } = createCtx()
@@ -1,5 +1,10 @@
import { describe, expect, it } from "bun:test"
import { activeUserMessageID, messageTurns, queuedUserMessageIDs } from "../../webview-ui/src/context/session-queue"
import {
activeUserMessageID,
messageTurns,
queuedUserMessageIDs,
stableMessageTurns,
} from "../../webview-ui/src/context/session-queue"
import type { Message } from "../../webview-ui/src/types/messages"
const base = {
@@ -84,6 +89,56 @@ describe("messageTurns", () => {
{ user: "message_4", assistant: [] },
])
})
it("surfaces leading assistant output as partial turns grouped by parent", () => {
const messages = [
assistant("message_2", "message_1"),
assistant("message_4", "message_3"),
assistant("message_5", "message_3"),
user("message_6"),
]
const turns = messageTurns(messages)
expect(
turns.map((turn) => ({ id: turn.id, partial: turn.partial, assistant: turn.assistant.map((msg) => msg.id) })),
).toEqual([
{ id: "message_1", partial: true, assistant: ["message_2"] },
{ id: "message_3", partial: true, assistant: ["message_4", "message_5"] },
{ id: "message_6", partial: undefined, assistant: [] },
])
})
})
describe("stableMessageTurns", () => {
it("keeps existing turn identities stable when older turns are prepended", () => {
const u1 = user("message_1")
const a2 = assistant("message_2", "message_1")
const u3 = user("message_3")
const prev = messageTurns([u1, a2, u3])
const next = stableMessageTurns(messageTurns([user("message_0"), u1, a2, u3]), prev)
expect(next[1]).toBe(prev[0])
expect(next[2]).toBe(prev[1])
})
it("replaces a turn identity when its assistant messages change", () => {
const u1 = user("message_1")
const a2 = assistant("message_2", "message_1")
const prev = messageTurns([u1, a2])
const next = stableMessageTurns(messageTurns([u1, a2, assistant("message_3", "message_1")]), prev)
expect(next[0]).not.toBe(prev[0])
expect(next[0]?.assistant.map((msg) => msg.id)).toEqual(["message_2", "message_3"])
})
it("keeps partial turn identities stable while their assistant messages are unchanged", () => {
const a2 = assistant("message_2", "message_1")
const a3 = assistant("message_3", "message_1")
const prev = messageTurns([a2, a3])
const next = stableMessageTurns(messageTurns([a2, a3, user("message_4")]), prev)
expect(next[0]).toBe(prev[0])
})
})
describe("activeUserMessageID", () => {
@@ -381,10 +381,10 @@ describe("constants", () => {
})
it("KNOWN_KEYS matches all keys in the Config interface (drift guard)", async () => {
// Read the Config interface from messages.ts and extract its keys.
// Read the Config interface from messages/config.ts and extract its keys.
// If someone adds a new field to Config, this test fails as a reminder
// to also add it to KNOWN_KEYS in settings-io.ts.
const src = await Bun.file(require("path").join(__dirname, "../../webview-ui/src/types/messages.ts")).text()
const src = await Bun.file(require("path").join(__dirname, "../../webview-ui/src/types/messages/config.ts")).text()
const match = src.match(/export interface Config \{([^}]+)\}/)
expect(match).not.toBeNull()
const body = match![1]
@@ -0,0 +1,223 @@
import { describe, it, expect } from "bun:test"
import { createTabOrderSync, type TabOrderSyncDeps } from "../../webview-ui/agent-manager/tab-order-sync"
import { applyTabOrder } from "../../webview-ui/agent-manager/tab-order"
// Builds a simulated AgentManager tab state with controllable accessors.
// Mirrors the real call order: source state (localSessionIDs, terminals)
// is mutated BEFORE the factory method is invoked.
function scene(init: {
order?: Record<string, string[]>
sessions?: string[]
worktreeSessions?: { key: string; ids: string[] }[]
review?: Record<string, boolean>
terminals?: Record<string, string[]>
}) {
const state = {
order: { ...(init.order ?? {}) } as Record<string, string[]>,
localIds: [...(init.sessions ?? [])],
worktreeSessions: init.worktreeSessions ?? [],
review: init.review ?? {},
terminals: { ...(init.terminals ?? {}) } as Record<string, string[]>,
persisted: [] as { key: string; order: string[] }[],
}
const deps: TabOrderSyncDeps = {
LOCAL: "LOCAL",
REVIEW_TAB_ID: "review",
order: () => state.order,
setOrder: (u) => {
state.order = u(state.order)
},
persist: (key, order) => {
state.persisted.push({ key, order: [...order] })
},
localSessionIDs: () => state.localIds,
sessions: () =>
state.worktreeSessions.flatMap((w, wi) =>
w.ids.map((id, i) => ({ id, createdAt: new Date(1700000000000 + wi * 1000 + i).toISOString() })),
),
managedSessions: () => state.worktreeSessions.flatMap((w) => w.ids.map((id) => ({ id, worktreeId: w.key }))),
reviewOpenByContext: () => state.review,
terminalIdsFor: (key) => state.terminals[key] ?? [],
}
return { state, sync: createTabOrderSync(deps), deps }
}
// Simulate how `tabIds()` renders the final tab bar: base composed as
// `[...sessions, review, ...terminals]` and `applyTabOrder` layered on top.
function render(deps: TabOrderSyncDeps, key: string): string[] {
const sids =
key === deps.LOCAL
? deps.localSessionIDs()
: deps
.sessions()
.filter((s) => deps.managedSessions().some((ms) => ms.id === s.id && ms.worktreeId === key))
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
.map((s) => s.id)
const withReview = deps.reviewOpenByContext()[key] === true ? [...sids, deps.REVIEW_TAB_ID] : sids
const base = [...withReview, ...deps.terminalIdsFor(key)]
return applyTabOrder(
base.map((id) => ({ id })),
deps.order()[key],
).map((i) => i.id)
}
describe("createTabOrderSync.append", () => {
it("puts a new pending tab at the tail when a terminal already exists (regression)", () => {
// Setup: existing session s1, existing terminal t1, no stored order yet
// (terminal append was a no-op under the old logic because t1 was
// already in base). User presses Cmd+T → pending_1 added to local
// sessions first, then append is called.
const { state, sync, deps } = scene({
sessions: ["s1", "pending_1"],
terminals: { LOCAL: ["t1"] },
})
sync.append("LOCAL", "pending_1")
expect(render(deps, "LOCAL")).toEqual(["s1", "t1", "pending_1"])
expect(state.order.LOCAL).toEqual(["s1", "t1", "pending_1"])
})
it("appends to tail when stored order exists and lacks the id", () => {
const { state, sync, deps } = scene({
order: { LOCAL: ["s1", "t1"] },
sessions: ["s1", "s2"],
terminals: { LOCAL: ["t1"] },
})
sync.append("LOCAL", "s2")
expect(state.order.LOCAL).toEqual(["s1", "t1", "s2"])
expect(render(deps, "LOCAL")).toEqual(["s1", "t1", "s2"])
})
it("moves the id to the tail if it was already present in stored", () => {
const { state, sync } = scene({
order: { LOCAL: ["s1", "s2", "t1"] },
sessions: ["s1", "s2"],
terminals: { LOCAL: ["t1"] },
})
sync.append("LOCAL", "s1")
expect(state.order.LOCAL).toEqual(["s2", "t1", "s1"])
})
it("resolves undefined key to LOCAL", () => {
const { state, sync } = scene({ sessions: ["s1"] })
sync.append(undefined, "s1")
expect(state.order.LOCAL).toEqual(["s1"])
})
it("handles an empty base (nothing mutated yet) by just writing the id", () => {
const { state, sync } = scene({})
sync.append("LOCAL", "x")
expect(state.order.LOCAL).toEqual(["x"])
})
})
describe("createTabOrderSync.replaceOrAppend", () => {
it("swaps a pending id for a real session id, preserving position", () => {
const { state, sync, deps } = scene({
order: { LOCAL: ["s1", "pending_1", "t1"] },
sessions: ["s1", "real_1"], // caller already mapped pending_1 → real_1
terminals: { LOCAL: ["t1"] },
})
sync.replaceOrAppend("LOCAL", "pending_1", "real_1")
expect(state.order.LOCAL).toEqual(["s1", "real_1", "t1"])
expect(render(deps, "LOCAL")).toEqual(["s1", "real_1", "t1"])
})
it("appends at the tail when the anchor isn't in stored", () => {
const { state, sync } = scene({
order: { LOCAL: ["s1", "t1"] },
sessions: ["s1", "s2"], // s2 not yet in stored
terminals: { LOCAL: ["t1"] },
})
sync.replaceOrAppend("LOCAL", "missing", "s2")
expect(state.order.LOCAL).toEqual(["s1", "t1", "s2"])
})
})
describe("createTabOrderSync.insertAfter", () => {
it("inserts a fork directly after its parent in the rendered order", () => {
const { state, sync, deps } = scene({
order: { LOCAL: ["s1", "t1"] },
sessions: ["s1", "child", "s2"], // caller already inserted child after s1
terminals: { LOCAL: ["t1"] },
})
sync.insertAfter("LOCAL", "s1", "child")
expect(state.order.LOCAL).toEqual(["s1", "child", "t1", "s2"])
// Rendered result has child immediately after s1.
expect(render(deps, "LOCAL")).toEqual(["s1", "child", "t1", "s2"])
})
it("appends when the anchor is missing entirely", () => {
const { state, sync } = scene({
order: { LOCAL: ["s1"] },
sessions: ["s1", "fork"],
})
sync.insertAfter("LOCAL", "missing_anchor", "fork")
expect(state.order.LOCAL).toEqual(["s1", "fork"])
})
it("seeds from base when no stored order exists (first fork)", () => {
const { state, sync } = scene({
sessions: ["s1", "child", "s2"],
})
sync.insertAfter("LOCAL", "s1", "child")
expect(state.order.LOCAL).toEqual(["s1", "child", "s2"])
})
})
describe("createTabOrderSync persistence filter", () => {
it("callers strip transient ids before persisting (review + terminal:* never hit disk)", () => {
// Mirror AgentManagerApp's real filter: strip review + terminal ids.
const { state, sync } = scene({
sessions: ["s1", "pending_1"],
terminals: { LOCAL: ["terminal:abc"] },
review: { LOCAL: true },
})
// Rebuild sync with a filtering persist to mimic the call site.
const filteredSync = createTabOrderSync({
LOCAL: "LOCAL",
REVIEW_TAB_ID: "review",
order: () => state.order,
setOrder: (u) => {
state.order = u(state.order)
},
persist: (key, order) => {
const clean = order.filter((id) => id !== "review" && !id.startsWith("terminal:"))
state.persisted.push({ key, order: clean })
},
localSessionIDs: () => state.localIds,
sessions: () => [],
managedSessions: () => [],
reviewOpenByContext: () => state.review,
terminalIdsFor: (key) => state.terminals[key] ?? [],
})
filteredSync.append("LOCAL", "pending_1")
// In-memory order still has terminals/review for drag state.
expect(state.order.LOCAL).toEqual(["s1", "review", "terminal:abc", "pending_1"])
// Persisted payload is session-only.
expect(state.persisted.at(-1)?.order).toEqual(["s1", "pending_1"])
})
})
describe("createTabOrderSync cross-method scenario", () => {
it("preserves position through terminal → pending → real lifecycle", () => {
const s = scene({ sessions: ["s1"] })
// 1. Terminal created — caller added t1 to terms state first.
s.state.terminals.LOCAL = ["t1"]
s.sync.append("LOCAL", "t1")
expect(s.state.order.LOCAL).toEqual(["s1", "t1"])
// 2. User presses Cmd+T → pending_1 appended to localIds first.
s.state.localIds = ["s1", "pending_1"]
s.sync.append("LOCAL", "pending_1")
expect(s.state.order.LOCAL).toEqual(["s1", "t1", "pending_1"])
// 3. Real session created → caller mapped pending_1 → real_1 in localIds.
s.state.localIds = ["s1", "real_1"]
s.sync.replaceOrAppend("LOCAL", "pending_1", "real_1")
expect(s.state.order.LOCAL).toEqual(["s1", "t1", "real_1"])
// Rendered tab bar keeps the new session in its slot (right of t1).
expect(render(s.deps, "LOCAL")).toEqual(["s1", "t1", "real_1"])
})
})
@@ -1,5 +1,11 @@
import { describe, it, expect } from "bun:test"
import { reorderTabs, applyTabOrder, firstOrderedTitle } from "../../webview-ui/agent-manager/tab-order"
import {
reorderTabs,
applyTabOrder,
firstOrderedTitle,
replaceInTabOrder,
insertInTabOrderAfter,
} from "../../webview-ui/agent-manager/tab-order"
describe("reorderTabs", () => {
const tabs = ["a", "b", "c", "d"]
@@ -146,6 +152,63 @@ describe("firstOrderedTitle", () => {
})
})
describe("replaceInTabOrder", () => {
it("replaces an id while preserving position", () => {
expect(replaceInTabOrder(["a", "b", "c"], "b", "B")).toEqual(["a", "B", "c"])
})
it("replaces at the head", () => {
expect(replaceInTabOrder(["a", "b"], "a", "A")).toEqual(["A", "b"])
})
it("replaces at the tail", () => {
expect(replaceInTabOrder(["a", "b"], "b", "B")).toEqual(["a", "B"])
})
it("returns undefined when oldId is not in order", () => {
expect(replaceInTabOrder(["a", "b"], "x", "X")).toBeUndefined()
})
it("returns undefined when order is undefined", () => {
expect(replaceInTabOrder(undefined, "a", "A")).toBeUndefined()
})
it("does not mutate the original array", () => {
const order = ["a", "b"]
replaceInTabOrder(order, "a", "A")
expect(order).toEqual(["a", "b"])
})
})
describe("insertInTabOrderAfter", () => {
it("inserts directly after the anchor", () => {
expect(insertInTabOrderAfter(["a", "b", "c"], "b", "B2")).toEqual(["a", "b", "B2", "c"])
})
it("inserts after the last item", () => {
expect(insertInTabOrderAfter(["a", "b"], "b", "c")).toEqual(["a", "b", "c"])
})
it("appends when anchor is missing", () => {
expect(insertInTabOrderAfter(["a", "b"], "x", "c")).toEqual(["a", "b", "c"])
})
it("appends when order is undefined", () => {
expect(insertInTabOrderAfter(undefined, "x", "c")).toEqual(["c"])
})
it("returns the base unchanged when id is already present", () => {
const order = ["a", "b"]
expect(insertInTabOrderAfter(order, "a", "b")).toBe(order)
})
it("does not mutate the original array", () => {
const order = ["a", "b"]
insertInTabOrderAfter(order, "a", "c")
expect(order).toEqual(["a", "b"])
})
})
// Helper: simulate reconciliation the same way handleDragOver does
function reconcile(current: string[], stored: string[]): string[] {
return applyTabOrder(
@@ -85,6 +85,7 @@ import { useLanguage } from "../src/context/language"
import { formatRelativeDate } from "../src/utils/date"
import { validateLocalSession, nextSelectionAfterDelete, adjacentHint, restoreLocalSessions, LOCAL } from "./navigate"
import { reorderTabs, applyTabOrder, firstOrderedTitle } from "./tab-order"
import { createTabOrderSync } from "./tab-order-sync"
import { ConstrainDragYAxis } from "./sortable-tab"
import { isTerminalTabId, createTerminalState, createTerminalHandlers, createTerminalMessageHandler } from "./terminal"
import { renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering"
@@ -667,17 +668,24 @@ const AgentManagerContent: Component = () => {
const [renamingSection, setRenamingSection] = createSignal<string | null>(null)
let pendingNewSection = false
/** Append an id to `worktreeTabOrder[key]` and persist server-side.
* Keeps new sessions/terminals at the right end of the tab bar
* instead of slipping in front of already-rendered tabs via
* tabIds()'s `[...sessions, review, ...terminals]` base order. */
const appendToTabOrder = (key: string, id: string) => {
const current = worktreeTabOrder()[key] ?? []
if (current.includes(id)) return
const next = [...current, id]
setWorktreeTabOrder((prev) => ({ ...prev, [key]: next }))
vscode.postMessage({ type: "agentManager.setTabOrder", key, order: next })
// Pin new tabs at the tail (see tab-order-sync); strip ephemeral ids so agent-manager.json stays clean.
const persistTabOrder = (key: string, order: string[]) => {
const durable = order.filter((id) => id !== REVIEW_TAB_ID && !isTerminalTabId(id))
vscode.postMessage({ type: "agentManager.setTabOrder", key, order: durable })
}
const tabOrderSync = createTabOrderSync({
LOCAL,
REVIEW_TAB_ID,
order: worktreeTabOrder,
setOrder: setWorktreeTabOrder,
persist: persistTabOrder,
localSessionIDs,
sessions: session.sessions,
managedSessions,
reviewOpenByContext,
terminalIdsFor: (key) => terms.forSelection(key).map((t) => t.id),
})
const appendToTabOrder = tabOrderSync.append
const addPendingTab = () => {
const id = `${PENDING_PREFIX}${++pendingCounter}`
@@ -1164,9 +1172,8 @@ const AgentManagerContent: Component = () => {
}
window.addEventListener("focus", onWindowFocus)
// When a session is created, add it as a local tab. This handles both direct
// creation from the prompt input and backend-created follow-up sessions (plan
// follow-up "Start new session"). Guard against duplicates (HTTP + SSE can both fire).
// Add created sessions as local tabs (both direct from the prompt and
// backend follow-ups). Dedups HTTP + SSE firing together.
const unsubCreate = vscode.onMessage((msg) => {
if (msg.type !== "sessionCreated") return
const created = msg as { type: string; session: { id: string } }
@@ -1175,10 +1182,12 @@ const AgentManagerContent: Component = () => {
const pending = selection() === LOCAL ? activePendingId() : undefined
if (pending) {
setLocalSessionIDs((prev) => prev.map((id) => (id === pending ? created.session.id : id)))
tabOrderSync.replaceOrAppend(LOCAL, pending, created.session.id)
setActivePendingId(undefined)
} else {
saveTabMemory()
setLocalSessionIDs((prev) => [...prev, created.session.id])
tabOrderSync.append(LOCAL, created.session.id)
setSelection(LOCAL)
}
vscode.postMessage({ type: "agentManager.persistSession", sessionId: created.session.id })
@@ -1196,8 +1205,7 @@ const AgentManagerContent: Component = () => {
setRunStatuses((prev) => ({ ...prev, [ev.worktreeId]: ev }))
})
// Terminal messages live in their own subscription to keep the main
// message handler's cyclomatic complexity under the lint cap.
// Terminal messages have their own subscription to keep main-handler complexity in check.
const terminalDispatch = createTerminalMessageHandler({
state: terms,
activate: termHandlers.activate,
@@ -1257,12 +1265,14 @@ const AgentManagerContent: Component = () => {
if (msg.type === "agentManager.sessionAdded") {
const ev = msg as { type: string; sessionId: string; worktreeId: string }
saveTabMemory()
appendToTabOrder(ev.worktreeId, ev.sessionId)
setSelection(ev.worktreeId)
session.selectSession(ev.sessionId)
}
if (msg.type === "agentManager.sessionForked") {
const ev = msg as { type: string; sessionId: string; forkedFromId: string; worktreeId?: string }
tabOrderSync.insertAfter(ev.worktreeId, ev.forkedFromId, ev.sessionId)
if (!ev.worktreeId) {
// Local session: insert new tab after the forked-from tab
setLocalSessionIDs((prev) => {
@@ -2075,18 +2085,18 @@ const AgentManagerContent: Component = () => {
const sel = selection()
if (sel === null) return
const key = sel === LOCAL ? LOCAL : sel
// Persist the full mixed order. Terminal IDs are ephemeral; on
// reload their PTYs are gone and `applyTabOrder` filters them out.
const order = worktreeTabOrder()[key]
if (order && order.length > 0) {
vscode.postMessage({ type: "agentManager.setTabOrder", key, order })
}
if (order && order.length > 0) persistTabOrder(key, order)
}
const draggedTab = createMemo(() => {
const id = draggingTab()
if (!id) return undefined
if (id === REVIEW_TAB_ID) return { id, title: t("session.tab.review") }
if (isTerminalTabId(id)) {
const term = terms.lookup().get(id)
return term ? { id, title: term.title } : undefined
}
return activeTabs().find((s) => s.id === id)
})
@@ -0,0 +1,83 @@
/**
* Factory for tab-order mutations used when sessions/terminals are created,
* a pending tab is promoted to a real session id, or a session is forked.
*
* Exists as a separate module to keep the tab-order branching out of
* AgentManagerApp's main message-handler arrow (complexity cap).
*/
import { replaceInTabOrder, insertInTabOrderAfter } from "./tab-order"
export interface TabOrderSyncDeps {
/** Constants that identify the local context and review tab. */
LOCAL: string
REVIEW_TAB_ID: string
/** Read/update the `contextKey → ordered tab ids` map (in-memory). */
order: () => Record<string, string[]>
setOrder: (updater: (prev: Record<string, string[]>) => Record<string, string[]>) => void
/** Persist to durable state. Callers should strip transient ids here. */
persist: (key: string, value: string[]) => void
/** State accessors used to rebuild the base order `[sessions, review, terminals]`. */
localSessionIDs: () => string[]
sessions: () => { id: string; createdAt: string }[]
managedSessions: () => { id: string; worktreeId?: string | null }[]
reviewOpenByContext: () => Record<string, boolean>
terminalIdsFor: (key: string) => string[]
}
export function createTabOrderSync(deps: TabOrderSyncDeps) {
const baseFor = (key: string): string[] => {
const sids =
key === deps.LOCAL
? deps.localSessionIDs()
: deps
.sessions()
.filter((s) => deps.managedSessions().some((ms) => ms.id === s.id && ms.worktreeId === key))
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
.map((s) => s.id)
const withReview = deps.reviewOpenByContext()[key] === true ? [...sids, deps.REVIEW_TAB_ID] : sids
return [...withReview, ...deps.terminalIdsFor(key)]
}
const commit = (key: string, next: string[]) => {
deps.setOrder((prev) => ({ ...prev, [key]: next }))
deps.persist(key, next)
}
const resolve = (key: string | undefined): string => key ?? deps.LOCAL
// Merge stored + any base ids not yet in stored — mirrors `applyTabOrder`'s
// output so we can pin `id` at a specific rendered position (tail for
// append, right-of-anchor for insertAfter) regardless of whether the
// caller already mutated source state (localSessionIDs, terms, etc).
const merge = (key: string): string[] => {
const stored = deps.order()[key] ?? []
const set = new Set(stored)
const unknowns = baseFor(key).filter((x) => !set.has(x))
return [...stored, ...unknowns]
}
return {
/** Place `id` at the tail of the persisted order for `key`. */
append(key: string | undefined, id: string) {
const k = resolve(key)
const rest = merge(k).filter((x) => x !== id)
commit(k, [...rest, id])
},
/** Swap `oldId` for `newId` preserving position, or append if missing. */
replaceOrAppend(key: string | undefined, oldId: string, newId: string) {
const k = resolve(key)
const stored = deps.order()[k] ?? []
const swapped = replaceInTabOrder(stored, oldId, newId)
if (swapped) return commit(k, swapped)
const rest = merge(k).filter((x) => x !== newId)
commit(k, [...rest, newId])
},
/** Place `id` directly after `anchorId`; append if anchor is missing. */
insertAfter(key: string | undefined, anchorId: string, id: string) {
const k = resolve(key)
const rest = merge(k).filter((x) => x !== id)
const next = insertInTabOrderAfter(rest, anchorId, id)
commit(k, next)
},
}
}
@@ -39,6 +39,33 @@ export function applyTabOrder<T extends { id: string }>(items: T[], order: strin
return ordered
}
/**
* Replace `oldId` with `newId` in `order`, preserving its position.
* Returns a new array, or undefined if `oldId` isn't in `order`.
* Used when a pending session tab is promoted to a real session id.
*/
export function replaceInTabOrder(order: string[] | undefined, oldId: string, newId: string): string[] | undefined {
if (!order) return undefined
const i = order.indexOf(oldId)
if (i === -1) return undefined
const next = [...order]
next[i] = newId
return next
}
/**
* Insert `id` into `order` directly after `afterId`.
* If `afterId` is missing from `order`, appends `id` at the end.
* Returns a new array, or undefined if `id` is already present.
*/
export function insertInTabOrderAfter(order: string[] | undefined, afterId: string, id: string): string[] {
const base = order ?? []
if (base.includes(id)) return base
const i = base.indexOf(afterId)
if (i === -1) return [...base, id]
return [...base.slice(0, i + 1), id, ...base.slice(i + 1)]
}
/**
* Find the title of the first item according to a custom order.
*
@@ -29,6 +29,8 @@ import {
activeUserMessageID as getActiveUserMessageID,
messageTurns,
queuedUserMessageIDs,
stableMessageTurns,
type MessageTurn,
} from "../../context/session-queue"
import type { QuestionRequest, SuggestionRequest } from "../../types/messages"
@@ -84,7 +86,9 @@ export const MessageList: Component<MessageListProps> = (props) => {
const positions = new Map<string, { top: number; userScrolled: boolean }>()
const boundary = () => session.revert()?.messageID
const turns = createMemo(() => messageTurns(session.messages(), boundary()))
const turns = createMemo((prev: MessageTurn[] | undefined) =>
stableMessageTurns(messageTurns(session.messages(), boundary()), prev),
)
const isEmpty = () => turns().length === 0 && !session.loading() && !boundary()
const recent = createMemo(() =>
@@ -151,11 +155,11 @@ export const MessageList: Component<MessageListProps> = (props) => {
if (pos?.userScrolled) {
el.scrollTop = pos.top
autoScroll.pause()
maybeLoadOlder()
} else {
autoScroll.forceScrollToBottom()
}
setPendingRestore(undefined)
maybeLoadOlder()
})
})
})
@@ -22,7 +22,9 @@ import { ModeSwitcher } from "../shared/ModeSwitcher"
import { ThinkingSelector } from "../shared/ThinkingSelector"
import { useFileMention } from "../../hooks/useFileMention"
import { useTerminalContext } from "../../hooks/useTerminalContext"
import { useGitChangesContext } from "../../hooks/useGitChangesContext"
import { hasTerminalMention } from "../../hooks/terminal-context-utils"
import { hasGitChangesMention } from "../../hooks/git-changes-context-utils"
import { useSlashCommand } from "../../hooks/useSlashCommand"
import { useGhostText } from "../../hooks/useGhostText"
import { useImageAttachments, type ImageAttachment } from "../../hooks/useImageAttachments"
@@ -65,11 +67,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const vscode = useVSCode()
const worktree = useWorktreeMode()
const dialog = useDialog()
const mention = useFileMention(
vscode,
() => session.currentSessionID() ?? props.pendingSessionID ?? session.draftSessionID(),
)
const sid = () => session.currentSessionID() ?? props.pendingSessionID ?? session.draftSessionID() ?? undefined
const ctx = () => {
const id = props.boxId
if (!id || !id.startsWith("agent-manager:")) return undefined
const rest = id.slice("agent-manager:".length)
return rest === "unassigned" ? undefined : rest
}
const hasGit = () => server.gitInstalled()
const mention = useFileMention(vscode, sid, hasGit)
const terminal = useTerminalContext(vscode)
const git = useGitChangesContext(vscode, ctx, hasGit)
const slash = useSlashCommand(vscode)
const imageAttach = useImageAttachments()
imageAttach.setFilePathDropHandler((paths) => {
@@ -277,13 +285,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const isBusy = () => isPromptBusy(session.status(), !!props.suggesting?.(), !!props.questioning?.())
const isDisabled = () => !server.isConnected()
const hasInput = () => text().trim().length > 0 || imageAttach.images().length > 0 || reviewComments().length > 0
const canSend = () => hasInput() && !isDisabled() && !terminal.pending() && !props.blocked?.()
const canSend = () => hasInput() && !isDisabled() && !terminal.pending() && !git.pending() && !props.blocked?.()
const showStop = () => isBusy() && !hasInput()
const isAtEnd = () =>
textareaRef ? atEnd(textareaRef.selectionStart, textareaRef.selectionEnd, textareaRef.value.length) : false
const highlightMentions = () => {
const paths = new Set(mention.mentionedPaths())
if (hasTerminalMention(text())) paths.add("terminal")
if (hasGit() && hasGitChangesMention(text())) paths.add("git-changes")
return paths
}
const placeholder = () => {
@@ -614,21 +623,33 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const pending = reviewComments()
const review = pending.length > 0 ? formatReviewCommentsMarkdown(pending) : ""
const message = draft && review ? `${review}\n\n${draft}` : draft || review
if ((!message && imgs.length === 0) || isDisabled() || terminal.pending() || props.blocked?.()) return
if ((!message && imgs.length === 0) || isDisabled() || terminal.pending() || git.pending() || props.blocked?.())
return
const mentionFiles = mention.parseFileAttachments(draft)
const imgFiles = imgs.map((img) => ({ mime: img.mime, url: img.dataUrl, filename: img.filename }))
const sel = session.selected()
const pendingId = props.pendingSessionID ?? session.draftSessionID()
const sid = session.currentSessionID()
const id = sid()
const terminalFile = await terminal.resolveAttachment(message, sid).catch((err: Error) => {
const terminalFile = await terminal.resolveAttachment(message, id).catch((err: Error) => {
showToast({ variant: "error", title: "Terminal context unavailable", description: err.message })
return undefined
})
if (hasTerminalMention(message) && !terminalFile) return
const allFiles = [...mentionFiles, ...imgFiles, ...(terminalFile ? [terminalFile] : [])]
const gitFile = await git.resolveAttachment(message, id).catch((err: Error) => {
showToast({ variant: "error", title: "Git changes unavailable", description: err.message })
return undefined
})
if (hasGit() && hasGitChangesMention(message) && !gitFile) return
const allFiles = [
...mentionFiles,
...imgFiles,
...(terminalFile ? [terminalFile] : []),
...(gitFile ? [gitFile] : []),
]
const attachments = allFiles.length > 0 ? allFiles : undefined
const key = draftKey()
@@ -736,6 +757,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<span class="file-mention-name">{item.label}</span>
<span class="file-mention-dir">{item.description}</span>
</>
) : item.type === "git-changes" ? (
<>
<Icon name="branch" class="file-mention-icon" />
<span class="file-mention-name">{item.label}</span>
<span class="file-mention-dir">{item.description}</span>
</>
) : (
<>
<FileIcon
@@ -50,6 +50,7 @@ export interface VscodeTurn {
id: string
user: WebMessage
assistant: WebMessage[]
partial?: boolean
}
interface VscodeSessionTurnProps {
@@ -71,7 +72,8 @@ export const VscodeSessionTurn: Component<VscodeSessionTurnProps> = (props) => {
createEffect(() => {
const turn = props.turn
session.hydrateParts([turn.user.id, ...turn.assistant.map((m) => m.id)])
const ids = turn.partial ? turn.assistant.map((m) => m.id) : [turn.user.id, ...turn.assistant.map((m) => m.id)]
session.hydrateParts(ids)
})
const message = createMemo(() => props.turn.user as SDKMessage & { role: "user" })
@@ -138,33 +140,35 @@ export const VscodeSessionTurn: Component<VscodeSessionTurnProps> = (props) => {
{(msg) => (
<div class="vscode-session-turn" data-message={msg().id}>
{/* User message */}
<div
class="vscode-session-turn-user"
data-revert-disabled={
assistantMessages().length > 0 && !session.revert() && session.status() !== "idle" ? "" : undefined
}
title={
assistantMessages().length > 0 && !session.revert() && session.status() !== "idle"
? language.t("revert.disabled.agentBusy")
: undefined
}
>
<UserMessageDisplay
message={msg() as unknown as Parameters<typeof UserMessageDisplay>[0]["message"]}
parts={parts() as unknown as Parameters<typeof UserMessageDisplay>[0]["parts"]}
interrupted={interrupted()}
queued={props.queued}
onFork={props.onForkMessage ? () => props.onForkMessage?.(msg().sessionID, msg().id) : undefined}
onRevert={
assistantMessages().length > 0 && !session.revert()
? () => {
if (session.status() !== "idle") return
session.revertSession(msg().id)
}
<Show when={!props.turn.partial}>
<div
class="vscode-session-turn-user"
data-revert-disabled={
assistantMessages().length > 0 && !session.revert() && session.status() !== "idle" ? "" : undefined
}
title={
assistantMessages().length > 0 && !session.revert() && session.status() !== "idle"
? language.t("revert.disabled.agentBusy")
: undefined
}
/>
</div>
>
<UserMessageDisplay
message={msg() as unknown as Parameters<typeof UserMessageDisplay>[0]["message"]}
parts={parts() as unknown as Parameters<typeof UserMessageDisplay>[0]["parts"]}
interrupted={interrupted()}
queued={props.queued}
onFork={props.onForkMessage ? () => props.onForkMessage?.(msg().sessionID, msg().id) : undefined}
onRevert={
assistantMessages().length > 0 && !session.revert()
? () => {
if (session.status() !== "idle") return
session.revertSession(msg().id)
}
: undefined
}
/>
</div>
</Show>
{/* Assistant parts — flat list, no context grouping */}
<Show when={assistantMessages().length > 0}>
@@ -14,6 +14,7 @@ import { createProviderAction } from "../../utils/provider-action"
interface ProviderConnectDialogProps {
providerID: string
oauthOnly?: boolean
}
interface ViewState {
@@ -49,7 +50,10 @@ const ProviderConnectDialog: Component<ProviderConnectDialogProps> = (props) =>
const item = createMemo(() => provider.providers()[props.providerID])
const name = () => item()?.name ?? props.providerID
const methods = createMemo<ProviderAuthMethod[]>(() => {
return provider.authMethods()[props.providerID] ?? fallbackMethods(language.t("provider.connect.method.apiKey"))
const list =
provider.authMethods()[props.providerID] ?? fallbackMethods(language.t("provider.connect.method.apiKey"))
if (props.oauthOnly) return list.filter((item) => item.type === "oauth")
return list
})
const method = createMemo(() => {
const index = state.methodIndex
@@ -75,6 +75,7 @@ const ProvidersTab: Component = () => {
if (cfg?.npm === "@ai-sdk/openai-compatible") return language.t("settings.providers.tag.custom")
return language.t("settings.providers.tag.config")
}
if (item.id === "openai" && current === "custom") return language.t("settings.providers.tag.chatgpt")
if (current === "custom") return language.t("settings.providers.tag.custom")
return language.t("settings.providers.tag.other")
}
@@ -121,6 +122,16 @@ const ProvidersTab: Component = () => {
dialog.show(() => <ProviderConnectDialog providerID={item.id} />)
}
function connectChatGPT(item: Provider) {
dialog.show(() => <ProviderConnectDialog providerID={item.id} oauthOnly />)
}
function chatgpt(item: Provider) {
if (item.id !== "openai") return false
if (source(item) === "custom") return false
return (provider.authMethods()[item.id] ?? []).some((method) => method.type === "oauth")
}
return (
<div>
{/* Kilo Gateway — always at the top, not editable */}
@@ -200,9 +211,8 @@ const ProvidersTab: Component = () => {
</span>
<Tag>{sourceTag(item)}</Tag>
</div>
<Show
when={canDisconnect(item)}
fallback={
<div style={{ display: "flex", "align-items": "center", gap: "4px" }}>
<Show when={!canDisconnect(item)}>
<span
style={{
"font-size": "14px",
@@ -212,9 +222,13 @@ const ProvidersTab: Component = () => {
>
{language.t("settings.providers.connected.environmentDescription")}
</span>
}
>
<div style={{ display: "flex", gap: "4px" }}>
</Show>
<Show when={chatgpt(item)}>
<Button size="large" variant="ghost" onClick={() => connectChatGPT(item)}>
{language.t("settings.providers.action.signInChatGPT")}
</Button>
</Show>
<Show when={canDisconnect(item)}>
<Show when={isCustom(item)}>
<Button size="large" variant="ghost" onClick={() => editProvider(item)}>
{language.t("provider.custom.edit.title")}
@@ -223,8 +237,8 @@ const ProvidersTab: Component = () => {
<Button size="large" variant="ghost" onClick={() => disconnect(item.id, item.name)}>
{language.t("common.disconnect")}
</Button>
</div>
</Show>
</Show>
</div>
</div>
)}
</For>
@@ -59,6 +59,37 @@ const Settings: Component<SettingsProps> = (props) => {
})
}
const open = (scope: "local" | "global") => {
const label =
scope === "global" ? language.t("settings.config.scope.global") : language.t("settings.config.scope.local")
vscode.postMessage({
type: "openConfigFile",
scope,
labels: {
scope: label,
statusLoaded: language.t("settings.config.status.loaded"),
statusLoadedLegacy: language.t("settings.config.status.loadedLegacy"),
statusNotLoaded: language.t("settings.config.status.notLoaded"),
statusCreate: language.t("settings.config.status.create"),
title: language.t("settings.config.title", { scope: label }),
placeholder: language.t("settings.config.placeholder"),
noWorkspace: language.t("settings.config.noWorkspace"),
openFailed: language.t("settings.config.openFailed", { scope: label, message: "{{message}}" }),
sourceXdg: language.t("settings.config.source.xdg"),
sourceHomeKilo: language.t("settings.config.source.homeKilo"),
sourceHomeKilocode: language.t("settings.config.source.homeKilocode"),
sourceHomeOpencode: language.t("settings.config.source.homeOpencode"),
sourceEnvFile: language.t("settings.config.source.envFile"),
sourceEnvDir: language.t("settings.config.source.envDir"),
sourceEnvContent: language.t("settings.config.source.envContent"),
sourceProjectKilo: language.t("settings.config.source.projectKilo"),
sourceProjectRoot: language.t("settings.config.source.projectRoot"),
sourceProjectKilocode: language.t("settings.config.source.projectKilocode"),
sourceProjectOpencode: language.t("settings.config.source.projectOpencode"),
},
})
}
// Sync when the parent changes the tab prop (e.g. via navigate message)
createEffect(
on(
@@ -84,10 +115,19 @@ const Settings: Component<SettingsProps> = (props) => {
"border-bottom": "1px solid var(--border-weak-base)",
display: "flex",
"align-items": "center",
"flex-wrap": "wrap",
gap: "8px",
}}
>
<h2 style={{ "font-size": "16px", "font-weight": "600", margin: 0 }}>{language.t("sidebar.settings")}</h2>
<h2 style={{ "font-size": "16px", "font-weight": "600", margin: 0, flex: 1 }}>
{language.t("sidebar.settings")}
</h2>
<Button variant="secondary" size="small" icon="edit" onClick={() => open("local")}>
{language.t("settings.openLocalConfig")}
</Button>
<Button variant="secondary" size="small" icon="edit" onClick={() => open("global")}>
{language.t("settings.openGlobalConfig")}
</Button>
</div>
{/* Settings tabs */}
@@ -4,10 +4,47 @@ export interface MessageTurn {
id: string
user: Message
assistant: Message[]
partial?: boolean
}
export function messageTurns(messages: Message[], boundary?: string) {
function key(msg: Message) {
return msg.parentID ?? msg.id
}
function partial(messages: Message[]): MessageTurn {
const first = messages[0]!
const id = first.parentID ?? `${first.id}:partial`
return {
id,
user: {
id,
sessionID: first.sessionID,
role: "user",
createdAt: first.createdAt,
time: first.time,
},
assistant: messages,
partial: true,
}
}
function partials(messages: Message[]): MessageTurn[] {
return messages
.reduce<Message[][]>((groups, msg) => {
const prev = groups[groups.length - 1]
if (!prev || key(prev[0]!) !== key(msg)) {
groups.push([msg])
return groups
}
prev.push(msg)
return groups
}, [])
.map(partial)
}
export function messageTurns(messages: Message[], boundary?: string): MessageTurn[] {
const result: MessageTurn[] = []
const lead: Message[] = []
const by = new Map<string, MessageTurn>()
for (const msg of messages) {
@@ -20,11 +57,43 @@ export function messageTurns(messages: Message[], boundary?: string) {
}
if (msg.role !== "assistant") continue
const turn = (msg.parentID ? by.get(msg.parentID) : undefined) ?? result[result.length - 1]
if (turn) turn.assistant.push(msg)
const turn = msg.parentID ? by.get(msg.parentID) : undefined
if (turn) {
turn.assistant.push(msg)
continue
}
const last = result[result.length - 1]
if (last) {
last.assistant.push(msg)
continue
}
lead.push(msg)
}
return result
if (lead.length === 0) return result
return [...partials(lead), ...result]
}
function sameMessages(a: Message[], b: Message[]) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false
}
return true
}
// Keep virtua's item keys stable across prepends; Solid's adapter keys by data object identity.
export function stableMessageTurns(next: MessageTurn[], prev: MessageTurn[] = []): MessageTurn[] {
if (prev.length === 0) return next
const by = new Map(prev.map((turn) => [turn.user.id, turn]))
return next.map((turn) => {
const old = by.get(turn.user.id)
if (!old) return turn
if (old.partial !== turn.partial) return turn
if (!turn.partial && old.user !== turn.user) return turn
if (!sameMessages(old.assistant, turn.assistant)) return turn
return old
})
}
function active(messages: Message[]) {
@@ -1,10 +1,12 @@
import type { FileAttachment, FileSearchItem } from "../types/messages"
import { GIT_CHANGES_MENTION } from "./git-changes-context-utils"
import { TERMINAL_MENTION } from "./terminal-context-utils"
export const AT_PATTERN = /(?:^|\s)@(\S*)$/
export type MentionResult =
| { type: "terminal"; value: typeof TERMINAL_MENTION; label: string; description: string }
| { type: "git-changes"; value: typeof GIT_CHANGES_MENTION; label: string; description: string }
| { type: "file"; value: string }
| { type: "folder"; value: string }
@@ -15,6 +17,13 @@ export const TERMINAL_RESULT: MentionResult = {
description: "Active terminal output",
}
export const GIT_CHANGES_RESULT: MentionResult = {
type: "git-changes",
value: GIT_CHANGES_MENTION,
label: "Git changes",
description: "Current session/worktree changes",
}
/**
* Escape special regex characters in a string so it can be used in a RegExp.
*/
@@ -28,13 +37,19 @@ export function getTerminalMentionResult(query: string): MentionResult[] {
return [TERMINAL_RESULT]
}
export function buildMentionResults(query: string, items: Array<FileSearchItem | string>): MentionResult[] {
export function getGitChangesMentionResult(query: string): MentionResult[] {
const normalized = query.toLowerCase()
if (normalized && !GIT_CHANGES_MENTION.startsWith(normalized) && !"git".startsWith(normalized)) return []
return [GIT_CHANGES_RESULT]
}
export function buildMentionResults(query: string, items: Array<FileSearchItem | string>, git = true): MentionResult[] {
const results: MentionResult[] = items.map((item) => {
if (typeof item === "string") return { type: "file", value: item }
if (item.type === "folder") return { type: "folder", value: item.path }
return { type: "file", value: item.path }
})
return [...getTerminalMentionResult(query), ...results]
return [...getTerminalMentionResult(query), ...(git ? getGitChangesMentionResult(query) : []), ...results]
}
/**
@@ -0,0 +1,42 @@
import type { FileAttachment } from "../types/messages"
export const GIT_CHANGES_MENTION = "git-changes"
export const GIT_CHANGES_FILENAME = "git-changes.txt"
export const GIT_CHANGES_PATTERN = /(^|\s)@git-changes(?=\s|$)/g
export type GitChangesMention = {
value: string
start: number
end: number
}
export function findGitChangesMention(text: string): GitChangesMention | undefined {
GIT_CHANGES_PATTERN.lastIndex = 0
const match = GIT_CHANGES_PATTERN.exec(text)
if (!match) return undefined
const prefix = match[1] ?? ""
const start = match.index + prefix.length
const value = `@${GIT_CHANGES_MENTION}`
return { value, start, end: start + value.length }
}
export function hasGitChangesMention(text: string): boolean {
return findGitChangesMention(text) !== undefined
}
export function buildGitChangesAttachment(text: string, content: string): FileAttachment | undefined {
const mention = findGitChangesMention(text)
if (!mention) return undefined
return {
mime: "text/plain",
url: `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`,
filename: GIT_CHANGES_FILENAME,
source: {
type: "file",
path: GIT_CHANGES_FILENAME,
text: mention,
},
}
}
@@ -42,7 +42,11 @@ export interface FileMention {
addPaths: (paths: string[], cwd: string) => void
}
export function useFileMention(vscode: VSCodeContext, sessionID?: Accessor<string | undefined>): FileMention {
export function useFileMention(
vscode: VSCodeContext,
sessionID?: Accessor<string | undefined>,
git?: Accessor<boolean>,
): FileMention {
const [mentionedPaths, setMentionedPaths] = createSignal<Set<string>>(new Set())
const [mentionQuery, setMentionQuery] = createSignal<string | null>(null)
const [mentionResults, setMentionResults] = createSignal<MentionResult[]>([])
@@ -63,7 +67,7 @@ export function useFileMention(vscode: VSCodeContext, sessionID?: Accessor<strin
if (message.requestId === `file-search-${fileSearchCounter}`) {
const items = message.items ?? message.paths.map((path) => ({ path, type: "file" as const }))
workspaceDir = message.dir
setMentionResults(buildMentionResults(mentionQuery() ?? "", items))
setMentionResults(buildMentionResults(mentionQuery() ?? "", items, git?.() ?? true))
setMentionIndex(0)
}
})
@@ -129,7 +133,7 @@ export function useFileMention(vscode: VSCodeContext, sessionID?: Accessor<strin
if (match) {
const query = match[1] ?? ""
setMentionQuery(query)
setMentionResults(buildMentionResults(query, []))
setMentionResults(buildMentionResults(query, [], git?.() ?? true))
requestFileSearch(query)
} else {
closeMention()
@@ -0,0 +1,86 @@
import { createSignal, onCleanup } from "solid-js"
import type { Accessor } from "solid-js"
import type { ExtensionMessage, FileAttachment, WebviewMessage } from "../types/messages"
import { buildGitChangesAttachment, hasGitChangesMention } from "./git-changes-context-utils"
const GIT_CHANGES_TIMEOUT_MS = 15_000
type Pending = {
resolve: (content: string) => void
reject: (err: Error) => void
timer: ReturnType<typeof setTimeout>
}
interface VSCodeContext {
postMessage: (message: WebviewMessage) => void
onMessage: (handler: (message: ExtensionMessage) => void) => () => void
}
export interface GitChangesContext {
pending: Accessor<boolean>
resolveAttachment: (text: string, sessionID?: string) => Promise<FileAttachment | undefined>
}
export function useGitChangesContext(
vscode: VSCodeContext,
context?: Accessor<string | undefined>,
git?: Accessor<boolean>,
): GitChangesContext {
const [pending, setPending] = createSignal(false)
const requests = new Map<string, Pending>()
let counter = 0
const settle = (requestId: string, run: (req: Pending) => void) => {
const req = requests.get(requestId)
if (!req) return
clearTimeout(req.timer)
requests.delete(requestId)
setPending(requests.size > 0)
run(req)
}
const unsubscribe = vscode.onMessage((message) => {
if (message.type === "gitChangesContextResult") {
settle(message.requestId, (req) => req.resolve(message.content))
return
}
if (message.type === "gitChangesContextError") {
settle(message.requestId, (req) => req.reject(new Error(message.error)))
}
})
onCleanup(() => {
unsubscribe()
for (const req of requests.values()) {
clearTimeout(req.timer)
req.reject(new Error("Git changes context request cancelled"))
}
requests.clear()
setPending(false)
})
const request = (sessionID?: string) =>
new Promise<string>((resolve, reject) => {
counter++
const requestId = `git-changes-context-${counter}`
const timer = setTimeout(() => {
settle(requestId, (req) => req.reject(new Error("Timed out while reading git changes")))
}, GIT_CHANGES_TIMEOUT_MS)
requests.set(requestId, { resolve, reject, timer })
setPending(true)
vscode.postMessage({ type: "requestGitChangesContext", requestId, sessionID, agentManagerContext: context?.() })
})
const resolveAttachment = async (text: string, sessionID?: string) => {
if (!hasGitChangesMention(text)) return undefined
if (git?.() === false) return undefined
const content = await request(sessionID)
return buildGitChangesAttachment(text, content)
}
return { pending, resolveAttachment }
}
+25
View File
@@ -734,10 +734,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "البيئة",
"settings.providers.tag.config": "التكوين",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "مخصص",
"settings.providers.tag.other": "أخرى",
"settings.providers.tag.customProvider": "مزود مخصص",
"settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك",
"settings.providers.action.signInChatGPT": "تسجيل الدخول باستخدام ChatGPT",
"settings.providers.custom.description": "أضف مزوداً متوافقاً مع OpenAI عبر عنوان URL الأساسي.",
"settings.providers.modeModels": "نموذج لكل وضع",
"settings.providers.custom.note": "أضف موفرًا متوافقًا مع OpenAI عبر عنوان URL الأساسي.",
@@ -820,6 +822,29 @@ export const dict = {
"provider.custom.error.baseURL.format": "يجب أن يبدأ بـ http:// أو https://",
"provider.custom.error.required": "مطلوب",
"provider.custom.error.duplicate": "مكرر",
"settings.openLocalConfig": "تكوين محلي",
"settings.openGlobalConfig": "تكوين عام",
"settings.config.scope.local": "محلي",
"settings.config.scope.global": "عالمي",
"settings.config.status.loaded": "محمل",
"settings.config.status.loadedLegacy": "محمل تكوين قديم",
"settings.config.status.notLoaded": "غير محمل",
"settings.config.status.create": "غير موجود - قم بإنشاء هذا الملف",
"settings.config.title": "فتح ملف تكوين Kilo {{scope}}",
"settings.config.placeholder": "يتم دمج ملفات التكوين بالترتيب؛ الملفات المحددة كمحملة تؤثر حاليًا على الإعدادات.",
"settings.config.noWorkspace": "افتح مجلد مساحة عمل لتحرير ملف تكوين Kilo المحلي.",
"settings.config.openFailed": "فشل فتح ملف تكوين Kilo {{scope}}: {{message}}",
"settings.config.source.xdg": "تكوين XDG العالمي",
"settings.config.source.homeKilo": "تكوين .kilo في Home",
"settings.config.source.homeKilocode": "تكوين .kilocode في Home",
"settings.config.source.homeOpencode": "تكوين .opencode في Home",
"settings.config.source.envFile": "ملف بيئة KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "تكوين البيئة المضمن",
"settings.config.source.projectKilo": "تكوين .kilo للمشروع",
"settings.config.source.projectRoot": "تكوين جذر المشروع",
"settings.config.source.projectKilocode": "تكوين .kilocode القديم",
"settings.config.source.projectOpencode": "تكوين .opencode القديم",
"settings.models.title": "النماذج",
"settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.",
"settings.agents.title": "الوكلاء",
+27
View File
@@ -741,10 +741,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Ambiente",
"settings.providers.tag.config": "Configuração",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Personalizado",
"settings.providers.tag.other": "Outro",
"settings.providers.tag.customProvider": "Provedor personalizado",
"settings.providers.connected.environmentDescription": "Conectado a partir das suas variáveis de ambiente",
"settings.providers.action.signInChatGPT": "Entrar com ChatGPT",
"settings.providers.custom.description": "Adicione um provedor compatível com OpenAI pela URL base.",
"settings.providers.modeModels": "Modelo por Modo",
"settings.providers.custom.note": "Adicione um provedor compatível com OpenAI por URL base.",
@@ -828,6 +830,31 @@ export const dict = {
"provider.custom.error.baseURL.format": "Deve começar com http:// ou https://",
"provider.custom.error.required": "Obrigatório",
"provider.custom.error.duplicate": "Duplicado",
"settings.openLocalConfig": "Config Local",
"settings.openGlobalConfig": "Config Global",
"settings.config.scope.local": "Local",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "carregado",
"settings.config.status.loadedLegacy": "configuração legada carregada",
"settings.config.status.notLoaded": "não carregado",
"settings.config.status.create": "não encontrado - criar este arquivo",
"settings.config.title": "Abrir arquivo de configuração {{scope}} do Kilo",
"settings.config.placeholder":
"Os arquivos de configuração são mesclados em ordem; os arquivos marcados como carregados afetam atualmente as configurações.",
"settings.config.noWorkspace":
"Abra uma pasta de espaço de trabalho para editar o arquivo de configuração local do Kilo.",
"settings.config.openFailed": "Falha ao abrir o arquivo de configuração {{scope}} do Kilo: {{message}}",
"settings.config.source.xdg": "Configuração global do XDG",
"settings.config.source.homeKilo": "Configuração .kilo da Home",
"settings.config.source.homeKilocode": "Configuração .kilocode da Home",
"settings.config.source.homeOpencode": "Configuração .opencode da Home",
"settings.config.source.envFile": "Arquivo de ambiente KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Configuração de ambiente embutida",
"settings.config.source.projectKilo": "Configuração .kilo do projeto",
"settings.config.source.projectRoot": "Configuração raiz do projeto",
"settings.config.source.projectKilocode": "Configuração .kilocode legada",
"settings.config.source.projectOpencode": "Configuração .opencode legada",
"settings.models.title": "Modelos",
"settings.models.description": "Configurações de modelos estarão disponíveis aqui.",
"settings.agents.title": "Agentes",
+26
View File
@@ -745,10 +745,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Okruženje",
"settings.providers.tag.config": "Konfiguracija",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Prilagođeno",
"settings.providers.tag.other": "Ostalo",
"settings.providers.tag.customProvider": "Prilagođeni provajder",
"settings.providers.connected.environmentDescription": "Povezano iz vaših varijabli okruženja",
"settings.providers.action.signInChatGPT": "Prijavi se putem ChatGPT",
"settings.providers.custom.description": "Dodaj OpenAI-kompatibilan provajder putem osnovnog URL-a.",
"settings.providers.modeModels": "Model po režimu",
"settings.providers.custom.note": "Dodajte provajdera kompatibilnog s OpenAI putem osnovnog URL-a.",
@@ -834,6 +836,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "Mora početi sa http:// ili https://",
"provider.custom.error.required": "Obavezno",
"provider.custom.error.duplicate": "Duplikat",
"settings.openLocalConfig": "Lokalna konfig.",
"settings.openGlobalConfig": "Globalna konfig.",
"settings.config.scope.local": "Lokalno",
"settings.config.scope.global": "Globalno",
"settings.config.status.loaded": "učitano",
"settings.config.status.loadedLegacy": "učitana zastarjela konfiguracija",
"settings.config.status.notLoaded": "nije učitano",
"settings.config.status.create": "nije pronađeno - kreiraj ovu datoteku",
"settings.config.title": "Otvori {{scope}} Kilo konfiguracijsku datoteku",
"settings.config.placeholder":
"Konfiguracijske datoteke se spajaju po redu; datoteke označene kao učitane trenutno utiču na postavke.",
"settings.config.noWorkspace": "Otvorite fasciklu radnog prostora da uredite lokalnu Kilo konfiguracijsku datoteku.",
"settings.config.openFailed": "Nije uspjelo otvaranje {{scope}} Kilo konfiguracijske datoteke: {{message}}",
"settings.config.source.xdg": "XDG globalna konfiguracija",
"settings.config.source.homeKilo": "Home .kilo konfiguracija",
"settings.config.source.homeKilocode": "Home .kilocode konfiguracija",
"settings.config.source.homeOpencode": "Home .opencode konfiguracija",
"settings.config.source.envFile": "KILO_CONFIG datoteka okruženja",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Ugrađena konfiguracija okruženja",
"settings.config.source.projectKilo": "Projektna .kilo konfiguracija",
"settings.config.source.projectRoot": "Konfiguracija korijena projekta",
"settings.config.source.projectKilocode": "Zastarjela .kilocode konfiguracija",
"settings.config.source.projectOpencode": "Zastarjela .opencode konfiguracija",
"settings.models.title": "Modeli",
"settings.models.description": "Postavke modela će se ovdje moći podešavati.",
"settings.agents.title": "Agenti",
+26
View File
@@ -740,10 +740,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Miljø",
"settings.providers.tag.config": "Konfiguration",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Brugerdefineret",
"settings.providers.tag.other": "Andet",
"settings.providers.tag.customProvider": "Brugerdefineret udbyder",
"settings.providers.connected.environmentDescription": "Forbundet fra dine miljøvariabler",
"settings.providers.action.signInChatGPT": "Log ind med ChatGPT",
"settings.providers.custom.description": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.",
"settings.providers.modeModels": "Model pr. tilstand",
"settings.providers.custom.note": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.",
@@ -827,6 +829,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "Skal starte med http:// eller https://",
"provider.custom.error.required": "Påkrævet",
"provider.custom.error.duplicate": "Duplikat",
"settings.openLocalConfig": "Lokal konfig",
"settings.openGlobalConfig": "Global konfig",
"settings.config.scope.local": "Lokal",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "indlæst",
"settings.config.status.loadedLegacy": "indlæst forældet konfiguration",
"settings.config.status.notLoaded": "ikke indlæst",
"settings.config.status.create": "ikke fundet - opret denne fil",
"settings.config.title": "Åbn {{scope}} Kilo konfigurationsfil",
"settings.config.placeholder":
"Konfigurationsfiler flettes i rækkefølge; filer markeret som indlæst påvirker i øjeblikket indstillingerne.",
"settings.config.noWorkspace": "Åbn en arbejdsområdemappe for at redigere den lokale Kilo konfigurationsfil.",
"settings.config.openFailed": "Kunne ikke åbne {{scope}} Kilo konfigurationsfil: {{message}}",
"settings.config.source.xdg": "XDG global konfiguration",
"settings.config.source.homeKilo": "Home .kilo konfiguration",
"settings.config.source.homeKilocode": "Home .kilocode konfiguration",
"settings.config.source.homeOpencode": "Home .opencode konfiguration",
"settings.config.source.envFile": "KILO_CONFIG miljøfil",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Indbygget miljøkonfiguration",
"settings.config.source.projectKilo": "Projekt .kilo konfiguration",
"settings.config.source.projectRoot": "Projekt rodkonfiguration",
"settings.config.source.projectKilocode": "Forældet .kilocode konfiguration",
"settings.config.source.projectOpencode": "Forældet .opencode konfiguration",
"settings.models.title": "Modeller",
"settings.models.description": "Modelindstillinger vil kunne konfigureres her.",
"settings.agents.title": "Agenter",
+27
View File
@@ -750,10 +750,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Umgebung",
"settings.providers.tag.config": "Konfiguration",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Benutzerdefiniert",
"settings.providers.tag.other": "Andere",
"settings.providers.tag.customProvider": "Benutzerdefinierter Anbieter",
"settings.providers.connected.environmentDescription": "Verbunden über Ihre Umgebungsvariablen",
"settings.providers.action.signInChatGPT": "Mit ChatGPT anmelden",
"settings.providers.custom.description": "Fügen Sie einen OpenAI-kompatiblen Anbieter über die Basis-URL hinzu.",
"settings.providers.modeModels": "Modell pro Modus",
"settings.providers.custom.note": "Fügen Sie einen OpenAI-kompatiblen Anbieter per Basis-URL hinzu.",
@@ -839,6 +841,31 @@ export const dict = {
"provider.custom.error.baseURL.format": "Muss mit http:// oder https:// beginnen",
"provider.custom.error.required": "Erforderlich",
"provider.custom.error.duplicate": "Duplikat",
"settings.openLocalConfig": "Lokale Config",
"settings.openGlobalConfig": "Globale Config",
"settings.config.scope.local": "Lokal",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "geladen",
"settings.config.status.loadedLegacy": "veraltete Konfiguration geladen",
"settings.config.status.notLoaded": "nicht geladen",
"settings.config.status.create": "nicht gefunden - diese Datei erstellen",
"settings.config.title": "{{scope}} Kilo-Konfigurationsdatei öffnen",
"settings.config.placeholder":
"Konfigurationsdateien werden der Reihe nach zusammengeführt; als geladen markierte Dateien wirken sich aktuell auf die Einstellungen aus.",
"settings.config.noWorkspace":
"Öffnen Sie einen Arbeitsbereichsordner, um die lokale Kilo-Konfigurationsdatei zu bearbeiten.",
"settings.config.openFailed": "Fehler beim Öffnen der {{scope}} Kilo-Konfigurationsdatei: {{message}}",
"settings.config.source.xdg": "Globale XDG-Konfiguration",
"settings.config.source.homeKilo": "Home .kilo-Konfiguration",
"settings.config.source.homeKilocode": "Home .kilocode-Konfiguration",
"settings.config.source.homeOpencode": "Home .opencode-Konfiguration",
"settings.config.source.envFile": "KILO_CONFIG Umgebungsdatei",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Inline-Umgebungskonfiguration",
"settings.config.source.projectKilo": "Projekt .kilo-Konfiguration",
"settings.config.source.projectRoot": "Projektstamm-Konfiguration",
"settings.config.source.projectKilocode": "Veraltete .kilocode-Konfiguration",
"settings.config.source.projectOpencode": "Veraltete .opencode-Konfiguration",
"settings.models.title": "Modelle",
"settings.models.description": "Modelleinstellungen können hier konfiguriert werden.",
"settings.agents.title": "Agenten",
+26 -1
View File
@@ -106,7 +106,7 @@ export const dict = {
"dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more",
"dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max",
"dialog.provider.copilot.note": "Claude models for coding assistance",
"dialog.provider.openai.note": "GPT models for fast, capable general AI tasks",
"dialog.provider.openai.note": "GPT and Codex models with API key or ChatGPT login",
"dialog.provider.google.note": "Gemini models for fast, structured responses",
"dialog.provider.openrouter.note": "Access all supported models from one provider",
"dialog.provider.vercel.note": "Unified access to AI models with smart routing",
@@ -744,10 +744,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Environment",
"settings.providers.tag.config": "Config",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Custom",
"settings.providers.tag.customProvider": "Custom provider",
"settings.providers.tag.other": "Other",
"settings.providers.connected.environmentDescription": "Connected from your environment variables",
"settings.providers.action.signInChatGPT": "Sign in with ChatGPT",
"settings.providers.custom.description": "Add an OpenAI-compatible provider by base URL.",
"provider.custom.title": "Custom provider",
@@ -828,6 +830,29 @@ export const dict = {
"provider.custom.error.baseURL.format": "Must start with http:// or https://",
"provider.custom.error.required": "Required",
"provider.custom.error.duplicate": "Duplicate",
"settings.openLocalConfig": "Local Config",
"settings.openGlobalConfig": "Global Config",
"settings.config.scope.local": "Local",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "loaded",
"settings.config.status.loadedLegacy": "loaded legacy config",
"settings.config.status.notLoaded": "not loaded",
"settings.config.status.create": "not found - create this file",
"settings.config.title": "Open {{scope}} Kilo config file",
"settings.config.placeholder": "Config files are merged in order; files marked loaded currently affect settings.",
"settings.config.noWorkspace": "Open a workspace folder to edit the local Kilo config file.",
"settings.config.openFailed": "Failed to open {{scope}} Kilo config file: {{message}}",
"settings.config.source.xdg": "XDG global config",
"settings.config.source.homeKilo": "Home .kilo config",
"settings.config.source.homeKilocode": "Home .kilocode config",
"settings.config.source.homeOpencode": "Home .opencode config",
"settings.config.source.envFile": "KILO_CONFIG environment file",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Inline environment config",
"settings.config.source.projectKilo": "Project .kilo config",
"settings.config.source.projectRoot": "Project root config",
"settings.config.source.projectKilocode": "Legacy .kilocode config",
"settings.config.source.projectOpencode": "Legacy .opencode config",
"settings.models.title": "Models",
"settings.models.description": "Model settings will be configurable here.",
"settings.agents.title": "Agents",
+27
View File
@@ -746,10 +746,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Entorno",
"settings.providers.tag.config": "Configuración",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Personalizado",
"settings.providers.tag.other": "Otro",
"settings.providers.tag.customProvider": "Proveedor personalizado",
"settings.providers.connected.environmentDescription": "Conectado desde tus variables de entorno",
"settings.providers.action.signInChatGPT": "Iniciar sesión con ChatGPT",
"settings.providers.custom.description": "Añade un proveedor compatible con OpenAI por URL base.",
"settings.providers.modeModels": "Modelo por modo",
"settings.providers.custom.note": "Agrega un proveedor compatible con OpenAI mediante URL base.",
@@ -834,6 +836,31 @@ export const dict = {
"provider.custom.error.baseURL.format": "Debe empezar con http:// o https://",
"provider.custom.error.required": "Obligatorio",
"provider.custom.error.duplicate": "Duplicado",
"settings.openLocalConfig": "Config local",
"settings.openGlobalConfig": "Config global",
"settings.config.scope.local": "Local",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "cargado",
"settings.config.status.loadedLegacy": "configuración heredada cargada",
"settings.config.status.notLoaded": "no cargado",
"settings.config.status.create": "no encontrado - crear este archivo",
"settings.config.title": "Abrir archivo de configuración Kilo {{scope}}",
"settings.config.placeholder":
"Los archivos de configuración se combinan en orden; los archivos marcados como cargados afectan actualmente a los ajustes.",
"settings.config.noWorkspace":
"Abre una carpeta de espacio de trabajo para editar el archivo de configuración Kilo local.",
"settings.config.openFailed": "Error al abrir el archivo de configuración Kilo {{scope}}: {{message}}",
"settings.config.source.xdg": "Configuración global XDG",
"settings.config.source.homeKilo": "Configuración .kilo de Home",
"settings.config.source.homeKilocode": "Configuración .kilocode de Home",
"settings.config.source.homeOpencode": "Configuración .opencode de Home",
"settings.config.source.envFile": "Archivo de entorno KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Configuración de entorno en línea",
"settings.config.source.projectKilo": "Configuración .kilo del proyecto",
"settings.config.source.projectRoot": "Configuración raíz del proyecto",
"settings.config.source.projectKilocode": "Configuración heredada .kilocode",
"settings.config.source.projectOpencode": "Configuración heredada .opencode",
"settings.models.title": "Modelos",
"settings.models.description": "La configuración de modelos estará disponible aquí.",
"settings.agents.title": "Agentes",
+27
View File
@@ -750,10 +750,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Environnement",
"settings.providers.tag.config": "Configuration",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Personnalisé",
"settings.providers.tag.other": "Autre",
"settings.providers.tag.customProvider": "Fournisseur personnalisé",
"settings.providers.connected.environmentDescription": "Connecté depuis vos variables d'environnement",
"settings.providers.action.signInChatGPT": "Se connecter avec ChatGPT",
"settings.providers.custom.description": "Ajoutez un fournisseur compatible OpenAI par URL de base.",
"settings.providers.modeModels": "Modèle par mode",
"settings.providers.custom.note": "Ajoutez un fournisseur compatible OpenAI par URL de base.",
@@ -839,6 +841,31 @@ export const dict = {
"provider.custom.error.baseURL.format": "Doit commencer par http:// ou https://",
"provider.custom.error.required": "Requis",
"provider.custom.error.duplicate": "Doublon",
"settings.openLocalConfig": "Config locale",
"settings.openGlobalConfig": "Config globale",
"settings.config.scope.local": "Local",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "chargé",
"settings.config.status.loadedLegacy": "configuration obsolète chargée",
"settings.config.status.notLoaded": "non chargé",
"settings.config.status.create": "introuvable - créer ce fichier",
"settings.config.title": "Ouvrir le fichier de configuration Kilo {{scope}}",
"settings.config.placeholder":
"Les fichiers de configuration sont fusionnés dans l'ordre ; les fichiers marqués comme chargés affectent actuellement les paramètres.",
"settings.config.noWorkspace":
"Ouvrez un dossier d'espace de travail pour modifier le fichier de configuration Kilo local.",
"settings.config.openFailed": "Échec de l'ouverture du fichier de configuration Kilo {{scope}} : {{message}}",
"settings.config.source.xdg": "Configuration globale XDG",
"settings.config.source.homeKilo": "Configuration .kilo de Home",
"settings.config.source.homeKilocode": "Configuration .kilocode de Home",
"settings.config.source.homeOpencode": "Configuration .opencode de Home",
"settings.config.source.envFile": "Fichier d'environnement KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Configuration d'environnement en ligne",
"settings.config.source.projectKilo": "Configuration .kilo du projet",
"settings.config.source.projectRoot": "Configuration racine du projet",
"settings.config.source.projectKilocode": "Configuration obsolète .kilocode",
"settings.config.source.projectOpencode": "Configuration obsolète .opencode",
"settings.models.title": "Modèles",
"settings.models.description": "Les paramètres des modèles seront configurables ici.",
"settings.agents.title": "Agents",
+26
View File
@@ -739,10 +739,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "環境",
"settings.providers.tag.config": "設定",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "カスタム",
"settings.providers.tag.other": "その他",
"settings.providers.tag.customProvider": "カスタムプロバイダー",
"settings.providers.connected.environmentDescription": "環境変数から接続されています",
"settings.providers.action.signInChatGPT": "ChatGPT でサインイン",
"settings.providers.custom.description": "ベースURLでOpenAI互換プロバイダーを追加します。",
"settings.providers.modeModels": "モードごとのモデル",
"settings.providers.custom.note": "Base URL で OpenAI 互換プロバイダーを追加します。",
@@ -825,6 +827,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "http:// または https:// で始まる必要があります",
"provider.custom.error.required": "必須",
"provider.custom.error.duplicate": "重複",
"settings.openLocalConfig": "ローカル設定",
"settings.openGlobalConfig": "グローバル設定",
"settings.config.scope.local": "ローカル",
"settings.config.scope.global": "グローバル",
"settings.config.status.loaded": "読み込み済み",
"settings.config.status.loadedLegacy": "旧構成を読み込み済み",
"settings.config.status.notLoaded": "読み込まれていません",
"settings.config.status.create": "見つかりません - このファイルを作成する",
"settings.config.title": "{{scope}}のKilo構成ファイルを開く",
"settings.config.placeholder":
"構成ファイルは順番にマージされます。読み込み済みとしてマークされているファイルが現在設定に影響しています。",
"settings.config.noWorkspace": "ローカルのKilo構成ファイルを編集するには、ワークスペースフォルダーを開いてください。",
"settings.config.openFailed": "{{scope}}のKilo構成ファイルを開けませんでした: {{message}}",
"settings.config.source.xdg": "XDGグローバル構成",
"settings.config.source.homeKilo": "Homeの.kilo構成",
"settings.config.source.homeKilocode": "Homeの.kilocode構成",
"settings.config.source.homeOpencode": "Homeの.opencode構成",
"settings.config.source.envFile": "KILO_CONFIG環境ファイル",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "インライン環境構成",
"settings.config.source.projectKilo": "プロジェクトの.kilo構成",
"settings.config.source.projectRoot": "プロジェクトルート構成",
"settings.config.source.projectKilocode": "旧.kilocode構成",
"settings.config.source.projectOpencode": "旧.opencode構成",
"settings.models.title": "モデル",
"settings.models.description": "モデル設定はここで構成できます。",
"settings.agents.title": "エージェント",
+26
View File
@@ -739,10 +739,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "환경",
"settings.providers.tag.config": "구성",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "사용자 지정",
"settings.providers.tag.other": "기타",
"settings.providers.tag.customProvider": "사용자 정의 공급자",
"settings.providers.connected.environmentDescription": "환경 변수에서 연결됨",
"settings.providers.action.signInChatGPT": "ChatGPT로 로그인",
"settings.providers.custom.description": "기본 URL로 OpenAI 호환 공급자를 추가합니다.",
"settings.providers.modeModels": "모드별 모델",
"settings.providers.custom.note": "Base URL로 OpenAI 호환 공급자를 추가합니다.",
@@ -825,6 +827,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "http:// 또는 https://로 시작해야 합니다",
"provider.custom.error.required": "필수",
"provider.custom.error.duplicate": "중복",
"settings.openLocalConfig": "로컬 설정",
"settings.openGlobalConfig": "전역 설정",
"settings.config.scope.local": "로컬",
"settings.config.scope.global": "글로벌",
"settings.config.status.loaded": "로드됨",
"settings.config.status.loadedLegacy": "레거시 구성 로드됨",
"settings.config.status.notLoaded": "로드되지 않음",
"settings.config.status.create": "찾을 수 없음 - 이 파일 만들기",
"settings.config.title": "{{scope}} Kilo 구성 파일 열기",
"settings.config.placeholder":
"구성 파일은 순서대로 병합됩니다. 로드됨으로 표시된 파일이 현재 설정에 영향을 미칩니다.",
"settings.config.noWorkspace": "로컬 Kilo 구성 파일을 편집하려면 작업 영역 폴더를 엽니다.",
"settings.config.openFailed": "{{scope}} Kilo 구성 파일을 열지 못했습니다: {{message}}",
"settings.config.source.xdg": "XDG 글로벌 구성",
"settings.config.source.homeKilo": "Home .kilo 구성",
"settings.config.source.homeKilocode": "Home .kilocode 구성",
"settings.config.source.homeOpencode": "Home .opencode 구성",
"settings.config.source.envFile": "KILO_CONFIG 환경 파일",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "인라인 환경 구성",
"settings.config.source.projectKilo": "프로젝트 .kilo 구성",
"settings.config.source.projectRoot": "프로젝트 루트 구성",
"settings.config.source.projectKilocode": "레거시 .kilocode 구성",
"settings.config.source.projectOpencode": "레거시 .opencode 구성",
"settings.models.title": "모델",
"settings.models.description": "모델 설정은 여기서 구성할 수 있습니다.",
"settings.agents.title": "에이전트",
+26
View File
@@ -743,10 +743,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Omgeving",
"settings.providers.tag.config": "Config",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Aangepast",
"settings.providers.tag.customProvider": "Aangepaste provider",
"settings.providers.tag.other": "Overige",
"settings.providers.connected.environmentDescription": "Gekoppeld via je omgevingsvariabelen",
"settings.providers.action.signInChatGPT": "Inloggen met ChatGPT",
"settings.providers.custom.description": "Voeg een OpenAI-compatibele provider toe via basis-URL.",
"provider.custom.title": "Aangepaste provider",
@@ -828,6 +830,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "Moet beginnen met http:// of https://",
"provider.custom.error.required": "Vereist",
"provider.custom.error.duplicate": "Duplicaat",
"settings.openLocalConfig": "Lokale config",
"settings.openGlobalConfig": "Globale config",
"settings.config.scope.local": "Lokaal",
"settings.config.scope.global": "Globaal",
"settings.config.status.loaded": "geladen",
"settings.config.status.loadedLegacy": "verouderde configuratie geladen",
"settings.config.status.notLoaded": "niet geladen",
"settings.config.status.create": "niet gevonden - maak dit bestand",
"settings.config.title": "Open {{scope}} Kilo-configuratiebestand",
"settings.config.placeholder":
"Configuratiebestanden worden op volgorde samengevoegd; bestanden gemarkeerd als geladen hebben momenteel invloed op de instellingen.",
"settings.config.noWorkspace": "Open een werkruimtemap om het lokale Kilo-configuratiebestand te bewerken.",
"settings.config.openFailed": "Kan {{scope}} Kilo-configuratiebestand niet openen: {{message}}",
"settings.config.source.xdg": "XDG globale configuratie",
"settings.config.source.homeKilo": "Home .kilo-configuratie",
"settings.config.source.homeKilocode": "Home .kilocode-configuratie",
"settings.config.source.homeOpencode": "Home .opencode-configuratie",
"settings.config.source.envFile": "KILO_CONFIG omgevingsbestand",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Inline omgevingsconfiguratie",
"settings.config.source.projectKilo": "Project .kilo-configuratie",
"settings.config.source.projectRoot": "Project root configuratie",
"settings.config.source.projectKilocode": "Verouderde .kilocode-configuratie",
"settings.config.source.projectOpencode": "Verouderde .opencode-configuratie",
"settings.models.title": "Modellen",
"settings.models.description": "Model-instellingen zullen hier configureerbaar zijn.",
"settings.agents.title": "Agenten",
+26
View File
@@ -744,10 +744,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Miljø",
"settings.providers.tag.config": "Konfigurasjon",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Tilpasset",
"settings.providers.tag.other": "Annet",
"settings.providers.tag.customProvider": "Egendefinert leverandør",
"settings.providers.connected.environmentDescription": "Koblet til fra dine miljøvariabler",
"settings.providers.action.signInChatGPT": "Logg inn med ChatGPT",
"settings.providers.custom.description": "Legg til en OpenAI-kompatibel leverandør via basis-URL.",
"settings.providers.modeModels": "Modell per modus",
"settings.providers.custom.note": "Legg til en OpenAI-kompatibel leverandør via basis-URL.",
@@ -831,6 +833,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "Må starte med http:// eller https://",
"provider.custom.error.required": "Påkrevd",
"provider.custom.error.duplicate": "Duplikat",
"settings.openLocalConfig": "Lokal konfig",
"settings.openGlobalConfig": "Global konfig",
"settings.config.scope.local": "Lokal",
"settings.config.scope.global": "Global",
"settings.config.status.loaded": "lastet",
"settings.config.status.loadedLegacy": "lastet inn eldre konfigurasjon",
"settings.config.status.notLoaded": "ikke lastet",
"settings.config.status.create": "ikke funnet - opprett denne filen",
"settings.config.title": "Åpne {{scope}} Kilo-konfigurasjonsfil",
"settings.config.placeholder":
"Konfigurasjonsfiler slås sammen i rekkefølge; filer merket som lastet påvirker for øyeblikket innstillingene.",
"settings.config.noWorkspace": "Åpne en arbeidsområdemappe for å redigere den lokale Kilo-konfigurasjonsfilen.",
"settings.config.openFailed": "Klarte ikke å åpne {{scope}} Kilo-konfigurasjonsfil: {{message}}",
"settings.config.source.xdg": "XDG global konfigurasjon",
"settings.config.source.homeKilo": "Home .kilo-konfigurasjon",
"settings.config.source.homeKilocode": "Home .kilocode-konfigurasjon",
"settings.config.source.homeOpencode": "Home .opencode-konfigurasjon",
"settings.config.source.envFile": "KILO_CONFIG miljøfil",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Innebygd miljøkonfigurasjon",
"settings.config.source.projectKilo": "Prosjekt .kilo-konfigurasjon",
"settings.config.source.projectRoot": "Prosjektets rotkonfigurasjon",
"settings.config.source.projectKilocode": "Eldre .kilocode-konfigurasjon",
"settings.config.source.projectOpencode": "Eldre .opencode-konfigurasjon",
"settings.models.title": "Modeller",
"settings.models.description": "Modellinnstillinger vil kunne konfigureres her.",
"settings.agents.title": "Agenter",
+26
View File
@@ -743,10 +743,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Środowisko",
"settings.providers.tag.config": "Konfiguracja",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Niestandardowe",
"settings.providers.tag.other": "Inne",
"settings.providers.tag.customProvider": "Niestandardowy dostawca",
"settings.providers.connected.environmentDescription": "Połączony z twoich zmiennych środowiskowych",
"settings.providers.action.signInChatGPT": "Zaloguj przez ChatGPT",
"settings.providers.custom.description": "Dodaj dostawcę kompatybilnego z OpenAI przez bazowy URL.",
"settings.providers.modeModels": "Model na tryb",
"settings.providers.custom.note": "Dodaj dostawcę kompatybilnego z OpenAI przez bazowy URL.",
@@ -832,6 +834,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "Musi zaczynać się od http:// lub https://",
"provider.custom.error.required": "Wymagane",
"provider.custom.error.duplicate": "Duplikat",
"settings.openLocalConfig": "Konfig. lokalna",
"settings.openGlobalConfig": "Konfig. globalna",
"settings.config.scope.local": "Lokalne",
"settings.config.scope.global": "Globalne",
"settings.config.status.loaded": "wczytano",
"settings.config.status.loadedLegacy": "wczytano przestarzałą konfigurację",
"settings.config.status.notLoaded": "nie wczytano",
"settings.config.status.create": "nie znaleziono - utwórz ten plik",
"settings.config.title": "Otwórz plik konfiguracyjny Kilo ({{scope}})",
"settings.config.placeholder":
"Pliki konfiguracyjne są scalane po kolei; pliki oznaczone jako wczytane mają obecnie wpływ na ustawienia.",
"settings.config.noWorkspace": "Otwórz folder obszaru roboczego, aby edytować lokalny plik konfiguracyjny Kilo.",
"settings.config.openFailed": "Nie udało się otworzyć pliku konfiguracyjnego Kilo ({{scope}}): {{message}}",
"settings.config.source.xdg": "Globalna konfiguracja XDG",
"settings.config.source.homeKilo": "Konfiguracja .kilo (Home)",
"settings.config.source.homeKilocode": "Konfiguracja .kilocode (Home)",
"settings.config.source.homeOpencode": "Konfiguracja .opencode (Home)",
"settings.config.source.envFile": "Plik środowiskowy KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Wbudowana konfiguracja środowiska",
"settings.config.source.projectKilo": "Konfiguracja .kilo projektu",
"settings.config.source.projectRoot": "Konfiguracja główna projektu",
"settings.config.source.projectKilocode": "Przestarzała konfiguracja .kilocode",
"settings.config.source.projectOpencode": "Przestarzała konfiguracja .opencode",
"settings.models.title": "Modele",
"settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.",
"settings.agents.title": "Agenci",
+27
View File
@@ -745,10 +745,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Среда",
"settings.providers.tag.config": "Конфигурация",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Пользовательский",
"settings.providers.tag.other": "Другое",
"settings.providers.tag.customProvider": "Пользовательский провайдер",
"settings.providers.connected.environmentDescription": "Подключён из ваших переменных окружения",
"settings.providers.action.signInChatGPT": "Войти через ChatGPT",
"settings.providers.custom.description": "Добавьте OpenAI-совместимый провайдер по базовому URL.",
"settings.providers.modeModels": "Модель для режима",
"settings.providers.custom.note": "Добавьте OpenAI-совместимого провайдера по базовому URL.",
@@ -833,6 +835,31 @@ export const dict = {
"provider.custom.error.baseURL.format": "Должен начинаться с http:// или https://",
"provider.custom.error.required": "Обязательно",
"provider.custom.error.duplicate": "Дубликат",
"settings.openLocalConfig": "Локальный конфиг",
"settings.openGlobalConfig": "Глобальный конфиг",
"settings.config.scope.local": "Локальный",
"settings.config.scope.global": "Глобальный",
"settings.config.status.loaded": "загружено",
"settings.config.status.loadedLegacy": "загружена устаревшая конфигурация",
"settings.config.status.notLoaded": "не загружено",
"settings.config.status.create": "не найдено - создать этот файл",
"settings.config.title": "Открыть файл конфигурации Kilo ({{scope}})",
"settings.config.placeholder":
"Файлы конфигурации объединяются по порядку; файлы, отмеченные как загруженные, в данный момент влияют на настройки.",
"settings.config.noWorkspace":
"Откройте папку рабочей области для редактирования локального файла конфигурации Kilo.",
"settings.config.openFailed": "Не удалось открыть файл конфигурации Kilo ({{scope}}): {{message}}",
"settings.config.source.xdg": "Глобальная конфигурация XDG",
"settings.config.source.homeKilo": "Конфигурация .kilo (Home)",
"settings.config.source.homeKilocode": "Конфигурация .kilocode (Home)",
"settings.config.source.homeOpencode": "Конфигурация .opencode (Home)",
"settings.config.source.envFile": "Файл среды KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Встроенная конфигурация среды",
"settings.config.source.projectKilo": "Конфигурация .kilo проекта",
"settings.config.source.projectRoot": "Корневая конфигурация проекта",
"settings.config.source.projectKilocode": "Устаревшая конфигурация .kilocode",
"settings.config.source.projectOpencode": "Устаревшая конфигурация .opencode",
"settings.models.title": "Модели",
"settings.models.description": "Настройки моделей будут доступны здесь.",
"settings.agents.title": "Агенты",
+26
View File
@@ -737,10 +737,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "สภาพแวดล้อม",
"settings.providers.tag.config": "กำหนดค่า",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "กำหนดเอง",
"settings.providers.tag.other": "อื่น ๆ",
"settings.providers.tag.customProvider": "ผู้ให้บริการที่กำหนดเอง",
"settings.providers.connected.environmentDescription": "เชื่อมต่อจากตัวแปรสภาพแวดล้อมของคุณ",
"settings.providers.action.signInChatGPT": "ลงชื่อเข้าใช้ด้วย ChatGPT",
"settings.providers.custom.description": "เพิ่มผู้ให้บริการที่เข้ากันได้กับ OpenAI ด้วย URL พื้นฐาน",
"settings.providers.modeModels": "โมเดลต่อโหมด",
"settings.providers.custom.note": "เพิ่มผู้ให้บริการที่รองรับ OpenAI ด้วย Base URL",
@@ -824,6 +826,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "ต้องขึ้นต้นด้วย http:// หรือ https://",
"provider.custom.error.required": "จำเป็น",
"provider.custom.error.duplicate": "ซ้ำ",
"settings.openLocalConfig": "คอนฟิก Local",
"settings.openGlobalConfig": "คอนฟิก Global",
"settings.config.scope.local": "ภายใน",
"settings.config.scope.global": "ทั่วโลก",
"settings.config.status.loaded": "โหลดแล้ว",
"settings.config.status.loadedLegacy": "โหลดการตั้งค่าแบบเก่าแล้ว",
"settings.config.status.notLoaded": "ยังไม่ได้โหลด",
"settings.config.status.create": "ไม่พบ - สร้างไฟล์นี้",
"settings.config.title": "เปิดไฟล์การตั้งค่า Kilo ({{scope}})",
"settings.config.placeholder":
"ไฟล์การตั้งค่าจะถูกผสานตามลำดับ ไฟล์ที่ถูกทำเครื่องหมายว่าโหลดแล้วจะมีผลกับการตั้งค่าในปัจจุบัน",
"settings.config.noWorkspace": "เปิดโฟลเดอร์พื้นที่ทำงานเพื่อแก้ไขไฟล์การตั้งค่า Kilo ภายใน",
"settings.config.openFailed": "ไม่สามารถเปิดไฟล์การตั้งค่า Kilo ({{scope}}): {{message}}",
"settings.config.source.xdg": "การตั้งค่า XDG ทั่วโลก",
"settings.config.source.homeKilo": "การตั้งค่า .kilo ของ Home",
"settings.config.source.homeKilocode": "การตั้งค่า .kilocode ของ Home",
"settings.config.source.homeOpencode": "การตั้งค่า .opencode ของ Home",
"settings.config.source.envFile": "ไฟล์สภาพแวดล้อม KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "การตั้งค่าสภาพแวดล้อมแบบอินไลน์",
"settings.config.source.projectKilo": "การตั้งค่า .kilo ของโปรเจกต์",
"settings.config.source.projectRoot": "การตั้งค่ารูทของโปรเจกต์",
"settings.config.source.projectKilocode": "การตั้งค่า .kilocode แบบเก่า",
"settings.config.source.projectOpencode": "การตั้งค่า .opencode แบบเก่า",
"settings.models.title": "โมเดล",
"settings.models.description": "การตั้งค่าโมเดลจะสามารถกำหนดค่าได้ที่นี่",
"settings.agents.title": "เอเจนต์",
+26
View File
@@ -744,10 +744,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Ortam",
"settings.providers.tag.config": "Yapılandırma",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Özel",
"settings.providers.tag.customProvider": "Özel sağlayıcı",
"settings.providers.tag.other": "Diğer",
"settings.providers.connected.environmentDescription": "Ortam değişkenlerinizden bağlandı",
"settings.providers.action.signInChatGPT": "ChatGPT ile oturum aç",
"settings.providers.custom.description": "Temel URL üzerinden OpenAI uyumlu bir sağlayıcı ekleyin.",
"provider.custom.title": "Özel sağlayıcı",
@@ -830,6 +832,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "http:// veya https:// ile başlamalı",
"provider.custom.error.required": "Gerekli",
"provider.custom.error.duplicate": "Tekrar",
"settings.openLocalConfig": "Yerel Config",
"settings.openGlobalConfig": "Global Config",
"settings.config.scope.local": "Yerel",
"settings.config.scope.global": "Küresel",
"settings.config.status.loaded": "yüklendi",
"settings.config.status.loadedLegacy": "eski yapılandırma yüklendi",
"settings.config.status.notLoaded": "yüklenmedi",
"settings.config.status.create": "bulunamadı - bu dosyayı oluştur",
"settings.config.title": "{{scope}} Kilo yapılandırma dosyasını aç",
"settings.config.placeholder":
"Yapılandırma dosyaları sırayla birleştirilir; yüklendi olarak işaretlenen dosyalar şu anda ayarları etkiler.",
"settings.config.noWorkspace": "Yerel Kilo yapılandırma dosyasını düzenlemek için bir çalışma alanı klasörü açın.",
"settings.config.openFailed": "{{scope}} Kilo yapılandırma dosyası açılamadı: {{message}}",
"settings.config.source.xdg": "XDG küresel yapılandırma",
"settings.config.source.homeKilo": "Home .kilo yapılandırması",
"settings.config.source.homeKilocode": "Home .kilocode yapılandırması",
"settings.config.source.homeOpencode": "Home .opencode yapılandırması",
"settings.config.source.envFile": "KILO_CONFIG ortam dosyası",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Satır içi ortam yapılandırması",
"settings.config.source.projectKilo": "Proje .kilo yapılandırması",
"settings.config.source.projectRoot": "Proje kök yapılandırması",
"settings.config.source.projectKilocode": "Eski .kilocode yapılandırması",
"settings.config.source.projectOpencode": "Eski .opencode yapılandırması",
"settings.models.title": "Modeller",
"settings.models.description": "Model ayarları burada yapılandırılabilecek.",
"settings.agents.title": "Ajanlar",
+26
View File
@@ -745,10 +745,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Середовище",
"settings.providers.tag.config": "Конфігурація",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "Власний",
"settings.providers.tag.customProvider": "Власний провайдер",
"settings.providers.tag.other": "Інші",
"settings.providers.connected.environmentDescription": "Підключено зі змінних середовища",
"settings.providers.action.signInChatGPT": "Увійти через ChatGPT",
"settings.providers.custom.description": "Додати OpenAI-сумісного провайдера через базовий URL.",
"provider.custom.title": "Власний провайдер",
@@ -830,6 +832,30 @@ export const dict = {
"provider.custom.error.baseURL.format": "Має починатися з http:// або https://",
"provider.custom.error.required": "Обов'язкове поле",
"provider.custom.error.duplicate": "Дублікат",
"settings.openLocalConfig": "Локальний конфіг",
"settings.openGlobalConfig": "Глобальний конфіг",
"settings.config.scope.local": "Локальний",
"settings.config.scope.global": "Глобальний",
"settings.config.status.loaded": "завантажено",
"settings.config.status.loadedLegacy": "завантажено застарілу конфігурацію",
"settings.config.status.notLoaded": "не завантажено",
"settings.config.status.create": "не знайдено - створити цей файл",
"settings.config.title": "Відкрити файл конфігурації Kilo ({{scope}})",
"settings.config.placeholder":
"Файли конфігурації об'єднуються по порядку; файли, позначені як завантажені, наразі впливають на налаштування.",
"settings.config.noWorkspace": "Відкрийте папку робочої області, щоб відредагувати локальний файл конфігурації Kilo.",
"settings.config.openFailed": "Не вдалося відкрити файл конфігурації Kilo ({{scope}}): {{message}}",
"settings.config.source.xdg": "Глобальна конфігурація XDG",
"settings.config.source.homeKilo": "Конфігурація .kilo (Home)",
"settings.config.source.homeKilocode": "Конфігурація .kilocode (Home)",
"settings.config.source.homeOpencode": "Конфігурація .opencode (Home)",
"settings.config.source.envFile": "Файл середовища KILO_CONFIG",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "Вбудована конфігурація середовища",
"settings.config.source.projectKilo": "Конфігурація .kilo проєкту",
"settings.config.source.projectRoot": "Коренева конфігурація проєкту",
"settings.config.source.projectKilocode": "Застаріла конфігурація .kilocode",
"settings.config.source.projectOpencode": "Застаріла конфігурація .opencode",
"settings.models.title": "Моделі",
"settings.models.description": "Тут можна буде налаштовувати параметри моделей.",
"settings.agents.title": "Агенти",
+25
View File
@@ -729,10 +729,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "环境",
"settings.providers.tag.config": "配置",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "自定义",
"settings.providers.tag.other": "其他",
"settings.providers.tag.customProvider": "自定义提供商",
"settings.providers.connected.environmentDescription": "从您的环境变量连接",
"settings.providers.action.signInChatGPT": "使用 ChatGPT 登录",
"settings.providers.custom.description": "通过基础 URL 添加 OpenAI 兼容的提供商。",
"settings.providers.modeModels": "按模式选择模型",
"settings.providers.custom.note": "通过 Base URL 添加 OpenAI 兼容提供商。",
@@ -814,6 +816,29 @@ export const dict = {
"provider.custom.error.baseURL.format": "必须以 http:// 或 https:// 开头",
"provider.custom.error.required": "必填",
"provider.custom.error.duplicate": "重复",
"settings.openLocalConfig": "本地配置",
"settings.openGlobalConfig": "全局配置",
"settings.config.scope.local": "本地",
"settings.config.scope.global": "全局",
"settings.config.status.loaded": "已加载",
"settings.config.status.loadedLegacy": "已加载旧版配置",
"settings.config.status.notLoaded": "未加载",
"settings.config.status.create": "未找到 - 创建此文件",
"settings.config.title": "打开 {{scope}} Kilo 配置文件",
"settings.config.placeholder": "配置文件按顺序合并;标记为已加载的文件目前会影响设置。",
"settings.config.noWorkspace": "打开工作区文件夹以编辑本地 Kilo 配置文件。",
"settings.config.openFailed": "无法打开 {{scope}} Kilo 配置文件:{{message}}",
"settings.config.source.xdg": "XDG 全局配置",
"settings.config.source.homeKilo": "主目录 .kilo 配置",
"settings.config.source.homeKilocode": "主目录 .kilocode 配置",
"settings.config.source.homeOpencode": "主目录 .opencode 配置",
"settings.config.source.envFile": "KILO_CONFIG 环境变量文件",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "内联环境配置",
"settings.config.source.projectKilo": "项目 .kilo 配置",
"settings.config.source.projectRoot": "项目根目录配置",
"settings.config.source.projectKilocode": "旧版 .kilocode 配置",
"settings.config.source.projectOpencode": "旧版 .opencode 配置",
"settings.models.title": "模型",
"settings.models.description": "模型设置将在此处可配置。",
"settings.agents.title": "智能体",
+25
View File
@@ -731,10 +731,12 @@ export const dict = {
"settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "環境",
"settings.providers.tag.config": "設定",
"settings.providers.tag.chatgpt": "ChatGPT",
"settings.providers.tag.custom": "自訂",
"settings.providers.tag.other": "其他",
"settings.providers.tag.customProvider": "自訂提供商",
"settings.providers.connected.environmentDescription": "從您的環境變數連線",
"settings.providers.action.signInChatGPT": "使用 ChatGPT 登入",
"settings.providers.custom.description": "透過基礎 URL 新增 OpenAI 相容的提供商。",
"settings.providers.modeModels": "按模式選擇模型",
"settings.providers.custom.note": "透過 Base URL 新增 OpenAI 相容供應商。",
@@ -816,6 +818,29 @@ export const dict = {
"provider.custom.error.baseURL.format": "必須以 http:// 或 https:// 開頭",
"provider.custom.error.required": "必填",
"provider.custom.error.duplicate": "重複",
"settings.openLocalConfig": "本機設定",
"settings.openGlobalConfig": "全域設定",
"settings.config.scope.local": "本地",
"settings.config.scope.global": "全域",
"settings.config.status.loaded": "已載入",
"settings.config.status.loadedLegacy": "已載入舊版設定",
"settings.config.status.notLoaded": "未載入",
"settings.config.status.create": "找不到 - 建立此檔案",
"settings.config.title": "開啟 {{scope}} Kilo 設定檔",
"settings.config.placeholder": "設定檔會按順序合併;標記為已載入的檔案目前會影響設定。",
"settings.config.noWorkspace": "開啟工作區資料夾以編輯本地 Kilo 設定檔。",
"settings.config.openFailed": "無法開啟 {{scope}} Kilo 設定檔:{{message}}",
"settings.config.source.xdg": "XDG 全域設定",
"settings.config.source.homeKilo": "主目錄 .kilo 設定",
"settings.config.source.homeKilocode": "主目錄 .kilocode 設定",
"settings.config.source.homeOpencode": "主目錄 .opencode 設定",
"settings.config.source.envFile": "KILO_CONFIG 環境變數檔案",
"settings.config.source.envDir": "KILO_CONFIG_DIR",
"settings.config.source.envContent": "內聯環境設定",
"settings.config.source.projectKilo": "專案 .kilo 設定",
"settings.config.source.projectRoot": "專案根目錄設定",
"settings.config.source.projectKilocode": "舊版 .kilocode 設定",
"settings.config.source.projectOpencode": "舊版 .opencode 設定",
"settings.models.title": "模型",
"settings.models.description": "可在此調整模型設定。",
"settings.agents.title": "代理程式",
@@ -679,6 +679,7 @@ const mockServer = {
vscodeLanguage: () => "en",
languageOverride: () => undefined,
workspaceDirectory: () => "/project",
gitInstalled: () => true,
}
export const WelcomeWithSwitcherAndNotification: Story = {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,191 @@
export type WorktreeErrorCode = "git_not_found" | "not_git_repo" | "lfs_missing"
// Agent Manager worktree state types (mirrored from WorktreeStateManager)
export interface WorktreeState {
id: string
branch: string
path: string
/** Bare branch name (e.g. "main"), without remote prefix. */
parentBranch: string
/** Remote name (e.g. "origin"). */
remote?: string
createdAt: string
/** Shared identifier for worktrees created together via multi-version mode. */
groupId?: string
/** User-provided display name for the worktree. */
label?: string
/** Cached PR number for instant badge display on reload. */
prNumber?: number
/** Cached PR URL for instant badge display on reload. */
prUrl?: string
/** Cached PR state for correct badge color on reload (open/merged/closed/draft). */
prState?: string
/** Section this worktree belongs to, or undefined for ungrouped. */
sectionId?: string
}
export interface SectionState {
id: string
name: string
/** Color label (e.g. "Red", "Blue") or null for default. */
color: string | null
order: number
collapsed: boolean
}
// ---------------------------------------------------------------------------
// PR status types (mirrored from extension types.ts)
// ---------------------------------------------------------------------------
export type PRState = "open" | "draft" | "merged" | "closed"
export type ReviewDecision = "approved" | "changes_requested" | "pending"
export type CheckStatus = "success" | "failure" | "pending" | "skipped" | "cancelled"
export type AggregateCheckStatus = "success" | "failure" | "pending" | "none"
export interface PRCheck {
name: string
status: CheckStatus
url?: string
duration?: string
}
export interface PRComment {
id: string
author: string
avatar?: string
body: string
file?: string
line?: number
url?: string
resolved: boolean
createdAt?: number
}
export interface PRStatus {
number: number
title: string
url: string
state: PRState
review: ReviewDecision | null
checks: {
status: AggregateCheckStatus
total: number
passed: number
failed: number
pending: number
items: PRCheck[]
}
comments?: {
total: number
unresolved: number
items: PRComment[]
}
additions: number
deletions: number
files: number
}
export type RunState = "idle" | "running" | "stopping"
export interface RunStatus {
worktreeId: string
state: RunState
exitCode?: number
signal?: string
startedAt?: string
finishedAt?: string
error?: string
}
export interface ManagedSessionState {
id: string
worktreeId: string | null
createdAt: string
}
export interface BranchInfo {
name: string
isLocal: boolean
isRemote: boolean
isDefault: boolean
lastCommitDate?: string
isCheckedOut?: boolean
}
// Agent Manager Import tab: external worktrees (extension → webview)
export interface ExternalWorktreeInfo {
path: string
branch: string
}
// Shared FileDiff shape (matches Snapshot.FileDiff from CLI backend)
export interface WorktreeFileDiff {
file: string
before: string
after: string
additions: number
deletions: number
status?: "added" | "deleted" | "modified"
tracked?: boolean
generatedLike?: boolean
summarized?: boolean
stamp?: string
}
export type AgentManagerApplyWorktreeDiffStatus = "checking" | "applying" | "success" | "conflict" | "error"
export interface AgentManagerApplyWorktreeDiffConflict {
file?: string
reason: string
}
// Per-worktree git stats: diff additions/deletions and ahead/behind counts
export interface WorktreeGitStats {
worktreeId: string
files: number
additions: number
deletions: number
ahead: number
behind: number
}
// Per-local-workspace git stats: branch name, diff additions/deletions, ahead/behind counts
export interface LocalGitStats {
branch: string
files: number
additions: number
deletions: number
ahead: number
behind: number
}
export interface ReviewComment {
id: string
file: string
side: "additions" | "deletions"
line: number
comment: string
selectedText: string
}
/**
* Maximum number of parallel worktree versions for multi-version mode.
* Keep in sync with MAX_MULTI_VERSIONS in src/agent-manager/constants.ts.
*/
export const MAX_MULTI_VERSIONS = 4
// Per-version model allocation for multi-model comparison mode
export interface ModelAllocation {
providerID: string
modelID: string
count: number
}
export type ContinueInWorktreeStatus =
| "capturing"
| "creating"
| "setup"
| "transferring"
| "forking"
| "done"
| "error"
@@ -0,0 +1,42 @@
import type { PermissionConfig, PermissionRuleItem } from "./permissions"
// Skill info from CLI backend
export interface SkillInfo {
name: string
description: string
location: string
}
// Slash command info from CLI backend
export interface SlashCommandInfo {
name: string
description?: string
source?: "command" | "mcp" | "skill"
hints: string[]
}
// Agent/mode info from CLI backend
export interface AgentInfo {
name: string
displayName?: string
description?: string
mode: "subagent" | "primary" | "all"
native?: boolean
hidden?: boolean
deprecated?: boolean
color?: string
permission?: PermissionRuleItem[]
}
export interface AgentConfig {
model?: string | null
prompt?: string
description?: string
mode?: "subagent" | "primary" | "all"
hidden?: boolean
disable?: boolean
temperature?: number
top_p?: number
steps?: number
permission?: PermissionConfig
}
@@ -0,0 +1,81 @@
import type { PermissionConfig } from "./permissions"
import type { AgentConfig } from "./agents"
import type { ProviderConfig } from "./providers"
export interface McpConfig {
type?: "local" | "remote"
command?: string[] | string
args?: string[]
env?: Record<string, string>
environment?: Record<string, string>
url?: string
headers?: Record<string, string>
enabled?: boolean
}
export interface CommandConfig {
template: string
description?: string
agent?: string
model?: string
}
export interface SkillsConfig {
paths?: string[]
urls?: string[]
}
export interface CompactionConfig {
auto?: boolean
prune?: boolean
}
export interface WatcherConfig {
ignore?: string[]
}
export interface ExperimentalConfig {
disable_paste_summary?: boolean
batch_tool?: boolean
codebase_search?: boolean
primary_tools?: string[]
continue_loop_on_deny?: boolean
mcp_timeout?: number
}
export interface CommitMessageConfig {
prompt?: string
}
export interface BrowserSettings {
enabled: boolean
useSystemChrome: boolean
headless: boolean
}
export interface Config {
permission?: PermissionConfig
model?: string | null
small_model?: string | null
default_agent?: string
agent?: Record<string, AgentConfig>
provider?: Record<string, ProviderConfig>
disabled_providers?: string[]
enabled_providers?: string[]
mcp?: Record<string, McpConfig>
command?: Record<string, CommandConfig>
instructions?: string[]
skills?: SkillsConfig
snapshot?: boolean
remote_control?: boolean
share?: "manual" | "auto" | "disabled"
username?: string
watcher?: WatcherConfig
formatter?: false | Record<string, unknown>
lsp?: false | Record<string, unknown>
compaction?: CompactionConfig
commit_message?: CommitMessageConfig
tools?: Record<string, boolean>
layout?: "auto" | "stretch"
experimental?: ExperimentalConfig
}
@@ -0,0 +1,30 @@
// Connection states
export type ConnectionState = "connecting" | "connected" | "disconnected" | "error"
// Session status (simplified from backend)
export type SessionStatus = "idle" | "busy" | "retry" | "offline"
// Rich status info for retry countdown and future extensions
export type SessionStatusInfo =
| { type: "idle" }
| { type: "busy" }
| { type: "retry"; attempt: number; message: string; next: number }
| { type: "offline"; message: string }
// Server info
export interface ServerInfo {
port: number
version?: string
}
// Device auth flow status
export type DeviceAuthStatus = "idle" | "initiating" | "pending" | "success" | "error" | "cancelled"
// Device auth state
export interface DeviceAuthState {
status: DeviceAuthStatus
code?: string
verificationUrl?: string
expiresIn?: number
error?: string
}
@@ -0,0 +1,921 @@
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@kilocode/sdk/v2/client"
import type { PartBatch, PartUpdate } from "../../../../src/shared/stream-messages"
import type { SessionMode } from "../../context/worktree-mode"
import type { MarketplaceItem, MarketplaceInstalledMetadata } from "../marketplace"
import type { ConnectionState, ServerInfo, SessionStatus } from "./connection"
import type { FileAttachment, Part } from "./parts"
import type { CloudSessionInfo, Message, MessageLoadMode, SessionInfo } from "./sessions"
import type { PermissionRequest } from "./permissions"
import type { QuestionRequest, SuggestionRequest, TodoItem } from "./questions"
import type { ModelSelection, Provider, ProviderAuthState } from "./providers"
import type { AgentInfo, SkillInfo, SlashCommandInfo } from "./agents"
import type { BrowserSettings, Config } from "./config"
import type { KilocodeNotification, ProfileData } from "./profile"
import type {
AgentManagerApplyWorktreeDiffConflict,
AgentManagerApplyWorktreeDiffStatus,
BranchInfo,
ContinueInWorktreeStatus,
ExternalWorktreeInfo,
LocalGitStats,
ManagedSessionState,
PRStatus,
ReviewComment,
RunStatus,
SectionState,
WorktreeErrorCode,
WorktreeFileDiff,
WorktreeGitStats,
WorktreeState,
} from "./agent-manager"
import type {
LegacyMigrationCompleteMessage,
LegacyMigrationDataMessage,
LegacyMigrationProgressMessage,
LegacyMigrationSessionProgressMessage,
MigrationStateMessage,
} from "./migration"
// ============================================
// Messages FROM extension TO webview
// ============================================
export interface ReadyMessage {
type: "ready"
serverInfo?: ServerInfo
extensionVersion?: string
vscodeLanguage?: string
languageOverride?: string
workspaceDirectory?: string
}
export interface GitStatusMessage {
type: "gitStatus"
repo: boolean
}
export interface WorkspaceDirectoryChangedMessage {
type: "workspaceDirectoryChanged"
directory: string
}
export interface LanguageChangedMessage {
type: "languageChanged"
locale: string
}
export interface ConnectionStateMessage {
type: "connectionState"
state: ConnectionState
error?: string
userMessage?: string
userDetails?: string
}
export interface ErrorMessage {
type: "error"
message: string
code?: string
sessionID?: string
}
export interface SendMessageFailedMessage {
type: "sendMessageFailed"
error: string
text: string
sessionID?: string
draftID?: string
messageID?: string
files?: FileAttachment[]
}
// Wire shape lives in src/shared/stream-messages.ts; narrow `part` to the
// webview's concrete union.
export type PartUpdatedMessage = PartUpdate<Part>
export type PartsUpdatedMessage = PartBatch<Part>
export interface SessionStatusMessage {
type: "sessionStatus"
sessionID: string
status: SessionStatus
// Retry fields (present when status === "retry")
attempt?: number
message?: string
next?: number
}
export interface SessionErrorMessage {
type: "sessionError"
sessionID?: string
error?: { name: string; data?: Record<string, unknown> }
}
export interface PermissionRequestMessage {
type: "permissionRequest"
permission: PermissionRequest
}
export interface PermissionResolvedMessage {
type: "permissionResolved"
permissionID: string
}
export interface PermissionErrorMessage {
type: "permissionError"
permissionID: string
}
export interface TodoUpdatedMessage {
type: "todoUpdated"
sessionID: string
items: TodoItem[]
}
export interface SessionCreatedMessage {
type: "sessionCreated"
session: SessionInfo
draftID?: string
}
export interface SessionForkedMessage {
type: "sessionForked"
sessionID: string
}
export interface SessionUpdatedMessage {
type: "sessionUpdated"
session: SessionInfo
}
export interface SessionDeletedMessage {
type: "sessionDeleted"
sessionID: string
}
export interface MessageRemovedMessage {
type: "messageRemoved"
sessionID: string
messageID: string
}
export interface MessagesLoadedMessage {
type: "messagesLoaded"
sessionID: string
messages: Message[]
mode?: Exclude<MessageLoadMode, "focus">
cursor?: string
hasMore?: boolean
}
export interface MessageCreatedMessage {
type: "messageCreated"
message: Message
}
export interface SessionsLoadedMessage {
type: "sessionsLoaded"
sessions: SessionInfo[]
preserveSessionIds?: string[]
}
export interface CloudSessionsLoadedMessage {
type: "cloudSessionsLoaded"
sessions: CloudSessionInfo[]
nextCursor: string | null
}
export interface GitRemoteUrlLoadedMessage {
type: "gitRemoteUrlLoaded"
gitUrl: string | null
}
export interface CloudSessionDataLoadedMessage {
type: "cloudSessionDataLoaded"
cloudSessionId: string
title: string
messages: Message[]
}
export interface CloudSessionImportedMessage {
type: "cloudSessionImported"
cloudSessionId: string
session: SessionInfo
}
export interface CloudSessionImportFailedMessage {
type: "cloudSessionImportFailed"
cloudSessionId: string
error: string
}
export interface OpenCloudSessionMessage {
type: "openCloudSession"
sessionId: string
}
export interface ActionMessage {
type: "action"
action: string
}
export interface SetChatBoxMessage {
type: "setChatBoxMessage"
text: string
}
export interface AppendChatBoxMessage {
type: "appendChatBoxMessage"
text: string
}
export interface AppendReviewCommentsMessage {
type: "appendReviewComments"
comments: ReviewComment[]
autoSend?: boolean
}
export interface TriggerTaskMessage {
type: "triggerTask"
text: string
}
export interface ProfileDataMessage {
type: "profileData"
data: ProfileData | null
}
export interface DeviceAuthStartedMessage {
type: "deviceAuthStarted"
code?: string
verificationUrl: string
expiresIn: number
}
export interface DeviceAuthCompleteMessage {
type: "deviceAuthComplete"
}
export interface DeviceAuthFailedMessage {
type: "deviceAuthFailed"
error: string
}
export interface DeviceAuthCancelledMessage {
type: "deviceAuthCancelled"
}
export interface NavigateMessage {
type: "navigate"
view: "newTask" | "marketplace" | "history" | "profile" | "settings" | "subAgentViewer"
tab?: string
}
export interface ProvidersLoadedMessage {
type: "providersLoaded"
providers: Record<string, Provider>
connected: string[]
defaults: Record<string, string>
defaultSelection: ModelSelection
authMethods: Record<string, ProviderAuthMethod[]>
authStates: Record<string, ProviderAuthState>
}
export interface AgentsLoadedMessage {
type: "agentsLoaded"
agents: AgentInfo[]
allAgents: AgentInfo[]
defaultAgent: string
}
export interface SkillsLoadedMessage {
type: "skillsLoaded"
skills: SkillInfo[]
}
export interface CommandsLoadedMessage {
type: "commandsLoaded"
commands: SlashCommandInfo[]
}
export interface AutocompleteSettingsLoadedMessage {
type: "autocompleteSettingsLoaded"
settings: {
enableAutoTrigger: boolean
enableSmartInlineTaskKeybinding: boolean
enableChatAutocomplete: boolean
}
}
export interface ChatCompletionResultMessage {
type: "chatCompletionResult"
text: string
requestId: string
}
export interface FileSearchItem {
path: string
type: "file" | "folder"
}
export interface FileSearchResultMessage {
type: "fileSearchResult"
paths: string[]
items?: FileSearchItem[]
dir: string
requestId: string
}
export interface TerminalContextResultMessage {
type: "terminalContextResult"
requestId: string
content: string
truncated?: boolean
}
export interface TerminalContextErrorMessage {
type: "terminalContextError"
requestId: string
error: string
}
export interface GitChangesContextResultMessage {
type: "gitChangesContextResult"
requestId: string
content: string
truncated?: boolean
}
export interface GitChangesContextErrorMessage {
type: "gitChangesContextError"
requestId: string
error: string
}
export interface QuestionRequestMessage {
type: "questionRequest"
question: QuestionRequest
}
export interface QuestionResolvedMessage {
type: "questionResolved"
requestID: string
}
export interface QuestionErrorMessage {
type: "questionError"
requestID: string
}
export interface SuggestionRequestMessage {
type: "suggestionRequest"
suggestion: SuggestionRequest
}
export interface SuggestionResolvedMessage {
type: "suggestionResolved"
requestID: string
}
export interface SuggestionErrorMessage {
type: "suggestionError"
requestID: string
}
export interface BrowserSettingsLoadedMessage {
type: "browserSettingsLoaded"
settings: BrowserSettings
}
export interface ClaudeCompatSettingLoadedMessage {
type: "claudeCompatSettingLoaded"
enabled: boolean
}
export interface ConfigLoadedMessage {
type: "configLoaded"
config: Config
}
export interface ConfigUpdatedMessage {
type: "configUpdated"
config: Config
}
export interface ConfigUpdateFailedMessage {
type: "configUpdateFailed"
message: string
details?: string
}
export interface GlobalConfigLoadedMessage {
type: "globalConfigLoaded"
config: Config
}
export interface NotificationSettingsLoadedMessage {
type: "notificationSettingsLoaded"
settings: {
notifyAgent: boolean
notifyPermissions: boolean
notifyErrors: boolean
soundAgent: string
soundPermissions: string
soundErrors: string
}
}
export interface TimelineSettingLoadedMessage {
type: "timelineSettingLoaded"
visible: boolean
}
export interface NotificationsLoadedMessage {
type: "notificationsLoaded"
notifications: KilocodeNotification[]
dismissedIds: string[]
}
// Agent Manager worktree session metadata
export interface AgentManagerSessionMetaMessage {
type: "agentManager.sessionMeta"
sessionId: string
mode: SessionMode
branch?: string
path?: string
parentBranch?: string
}
// Agent Manager repo info (current branch of the main workspace)
export interface AgentManagerRepoInfoMessage {
type: "agentManager.repoInfo"
branch: string
defaultBranch?: string
}
// Agent Manager worktree setup progress
export interface AgentManagerWorktreeSetupMessage {
type: "agentManager.worktreeSetup"
status: "creating" | "starting" | "ready" | "error"
message: string
sessionId?: string
branch?: string
worktreeId?: string
errorCode?: WorktreeErrorCode
}
// Agent Manager session added to an existing worktree (no setup overlay needed)
export interface AgentManagerSessionAddedMessage {
type: "agentManager.sessionAdded"
sessionId: string
worktreeId: string
}
// Agent Manager session forked from an existing session
export interface AgentManagerSessionForkedMessage {
type: "agentManager.sessionForked"
sessionId: string
forkedFromId: string
worktreeId?: string
}
// Full state push from extension to webview
export interface AgentManagerStateMessage {
type: "agentManager.state"
worktrees: WorktreeState[]
sessions: ManagedSessionState[]
sections?: SectionState[]
staleWorktreeIds?: string[]
tabOrder?: Record<string, string[]>
worktreeOrder?: string[]
sessionsCollapsed?: boolean
reviewDiffStyle?: "unified" | "split"
isGitRepo?: boolean
defaultBaseBranch?: string
runStatuses?: RunStatus[]
runScriptConfigured?: boolean
runScriptPath?: string
}
// ---------------------------------------------------------------------------
// Agent Manager terminal messages
// ---------------------------------------------------------------------------
export interface AgentManagerTerminalCreatedMessage {
type: "agentManager.terminal.created"
/** null for LOCAL, worktree id otherwise */
worktreeId: string | null
terminalId: string
title: string
wsUrl: string
}
export interface AgentManagerTerminalClosedMessage {
type: "agentManager.terminal.closed"
terminalId: string
}
export interface AgentManagerTerminalErrorMessage {
type: "agentManager.terminal.error"
terminalId?: string
message: string
}
export interface AgentManagerRunStatusMessage extends RunStatus {
type: "agentManager.runStatus"
}
// Resolved keybindings for agent manager actions
export interface AgentManagerKeybindingsMessage {
type: "agentManager.keybindings"
bindings: Record<string, string>
}
// Multi-version creation progress (extension → webview)
export interface AgentManagerMultiVersionProgressMessage {
type: "agentManager.multiVersionProgress"
status: "creating" | "done"
total: number
completed: number
groupId?: string
}
// Stored variant selections loaded from extension globalState (extension → webview)
export interface VariantsLoadedMessage {
type: "variantsLoaded"
variants: Record<string, string>
}
export interface RecentsLoadedMessage {
type: "recentsLoaded"
recents: ModelSelection[]
}
export interface FavoritesLoadedMessage {
type: "favoritesLoaded"
favorites: ModelSelection[]
}
// Per-mode model selections loaded from model.json (extension → webview)
export interface ModelSelectionsLoadedMessage {
type: "modelSelectionsLoaded"
selections: Record<string, ModelSelection>
}
export interface AgentManagerBranchesMessage {
type: "agentManager.branches"
branches: BranchInfo[]
defaultBranch: string
}
export interface AgentManagerExternalWorktreesMessage {
type: "agentManager.externalWorktrees"
worktrees: ExternalWorktreeInfo[]
}
// Agent Manager Import tab: result feedback (extension → webview)
export interface AgentManagerImportResultMessage {
type: "agentManager.importResult"
success: boolean
message: string
errorCode?: WorktreeErrorCode
}
// Agent Manager: Diff data push (extension → webview)
export interface AgentManagerWorktreeDiffMessage {
type: "agentManager.worktreeDiff"
sessionId: string
diffs: WorktreeFileDiff[]
}
export interface AgentManagerWorktreeDiffFileMessage {
type: "agentManager.worktreeDiffFile"
sessionId: string
file: string
diff: WorktreeFileDiff | null
}
// Agent Manager: Diff loading state (extension → webview)
export interface AgentManagerWorktreeDiffLoadingMessage {
type: "agentManager.worktreeDiffLoading"
sessionId: string
loading: boolean
}
export interface AgentManagerApplyWorktreeDiffResultMessage {
type: "agentManager.applyWorktreeDiffResult"
worktreeId: string
status: AgentManagerApplyWorktreeDiffStatus
message: string
conflicts?: AgentManagerApplyWorktreeDiffConflict[]
}
// Agent Manager: Revert single file result (extension → webview)
export interface AgentManagerRevertWorktreeFileResultMessage {
type: "agentManager.revertWorktreeFileResult"
sessionId: string
file: string
status: "success" | "error"
message: string
}
// Agent Manager: Worktree git stats push (extension → webview)
export interface AgentManagerWorktreeStatsMessage {
type: "agentManager.worktreeStats"
stats: WorktreeGitStats[]
}
// Agent Manager: Local workspace git stats push (extension → webview)
export interface AgentManagerLocalStatsMessage {
type: "agentManager.localStats"
stats: LocalGitStats
}
// Agent Manager: PR status push (extension → webview)
export interface AgentManagerPRStatusMessage {
type: "agentManager.prStatus"
worktreeId: string
pr: PRStatus | null
error?: "gh_missing" | "gh_auth" | "fetch_failed"
}
// Sidebar: Live worktree diff stats (extension → webview)
export interface WorktreeStatsLoadedMessage {
type: "worktreeStatsLoaded"
files: number
additions: number
deletions: number
}
// Set the model for a session (extension → webview, used during multi-version creation)
export interface AgentManagerSetSessionModelMessage {
type: "agentManager.setSessionModel"
sessionId: string
providerID: string
modelID: string
}
// Request webview to send initial prompt to a newly created session (extension → webview)
export interface AgentManagerSendInitialMessage {
type: "agentManager.sendInitialMessage"
sessionId: string
worktreeId: string
text?: string
providerID?: string
modelID?: string
agent?: string
files?: Array<{ mime: string; url: string }>
}
// Enhance prompt result (extension → webview)
export interface EnhancePromptResultMessage {
type: "enhancePromptResult"
text: string
requestId: string
}
// Enhance prompt error (extension → webview)
export interface EnhancePromptErrorMessage {
type: "enhancePromptError"
error: string
requestId: string
}
// Sub-agent viewer: open a child session in read-only mode (extension → webview)
export interface ViewSubAgentSessionMessage {
type: "viewSubAgentSession"
sessionID: string
}
export interface DiffViewerDiffsMessage {
type: "diffViewer.diffs"
diffs: WorktreeFileDiff[]
}
export interface DiffViewerLoadingMessage {
type: "diffViewer.loading"
loading: boolean
}
export interface DiffViewerRevertFileResultMessage {
type: "diffViewer.revertFileResult"
file: string
status: "success" | "error"
message: string
}
export interface ClearPendingPromptsMessage {
type: "clearPendingPrompts"
}
export interface ExtensionDataReadyMessage {
type: "extensionDataReady"
}
// ============================================
// Marketplace Messages
// ============================================
export interface MarketplaceDataMessage {
type: "marketplaceData"
marketplaceItems: MarketplaceItem[]
marketplaceInstalledMetadata: MarketplaceInstalledMetadata
errors?: string[]
}
export interface MarketplaceInstallResultMessage {
type: "marketplaceInstallResult"
success: boolean
slug: string
error?: string
}
export interface MarketplaceRemoveResultMessage {
type: "marketplaceRemoveResult"
success: boolean
slug: string
error?: string
}
export interface ProviderOAuthReadyMessage {
type: "providerOAuthReady"
requestId: string
providerID: string
authorization: ProviderAuthAuthorization
}
export interface ProviderConnectedMessage {
type: "providerConnected"
requestId: string
providerID: string
}
export interface ProviderDisconnectedMessage {
type: "providerDisconnected"
requestId: string
providerID: string
}
export interface ProviderActionErrorMessage {
type: "providerActionError"
requestId: string
providerID: string
action: "authorize" | "connect" | "disconnect"
message: string
}
export interface CustomProviderModelsFetchedMessage {
type: "customProviderModelsFetched"
requestId: string
models?: Array<{ id: string; name: string }>
error?: string
/** True when error was HTTP 401/403 — hints the user to check their API key */
auth?: boolean
}
export interface McpStatusEntry {
status: "connected" | "disabled" | "failed" | "needs_auth" | "needs_client_registration"
error?: string
}
export interface McpStatusLoadedMessage {
type: "mcpStatusLoaded"
status: Record<string, McpStatusEntry>
}
// Continue in Worktree: progress updates (extension → webview)
export interface ContinueInWorktreeProgressMessage {
type: "continueInWorktreeProgress"
status: ContinueInWorktreeStatus
detail?: string
error?: string
}
export interface RemoteStatusMessage {
type: "remoteStatus"
enabled: boolean
connected: boolean
}
export type ExtensionMessage =
| ReadyMessage
| GitStatusMessage
| ConnectionStateMessage
| ErrorMessage
| SendMessageFailedMessage
| PartUpdatedMessage
| PartsUpdatedMessage
| SessionStatusMessage
| SessionErrorMessage
| PermissionRequestMessage
| PermissionResolvedMessage
| PermissionErrorMessage
| TodoUpdatedMessage
| SessionCreatedMessage
| SessionForkedMessage
| SessionUpdatedMessage
| SessionDeletedMessage
| MessageRemovedMessage
| MessagesLoadedMessage
| MessageCreatedMessage
| SessionsLoadedMessage
| CloudSessionsLoadedMessage
| GitRemoteUrlLoadedMessage
| ActionMessage
| ProfileDataMessage
| DeviceAuthStartedMessage
| DeviceAuthCompleteMessage
| DeviceAuthFailedMessage
| DeviceAuthCancelledMessage
| NavigateMessage
| ProvidersLoadedMessage
| AgentsLoadedMessage
| SkillsLoadedMessage
| CommandsLoadedMessage
| AutocompleteSettingsLoadedMessage
| ChatCompletionResultMessage
| FileSearchResultMessage
| TerminalContextResultMessage
| TerminalContextErrorMessage
| GitChangesContextResultMessage
| GitChangesContextErrorMessage
| QuestionRequestMessage
| QuestionResolvedMessage
| QuestionErrorMessage
| SuggestionRequestMessage
| SuggestionResolvedMessage
| SuggestionErrorMessage
| BrowserSettingsLoadedMessage
| ClaudeCompatSettingLoadedMessage
| ConfigLoadedMessage
| ConfigUpdatedMessage
| ConfigUpdateFailedMessage
| GlobalConfigLoadedMessage
| NotificationSettingsLoadedMessage
| TimelineSettingLoadedMessage
| NotificationsLoadedMessage
| AgentManagerSessionMetaMessage
| AgentManagerRepoInfoMessage
| AgentManagerWorktreeSetupMessage
| AgentManagerSessionAddedMessage
| AgentManagerSessionForkedMessage
| AgentManagerStateMessage
| AgentManagerRunStatusMessage
| AgentManagerKeybindingsMessage
| AgentManagerMultiVersionProgressMessage
| AgentManagerSetSessionModelMessage
| AgentManagerSendInitialMessage
| SetChatBoxMessage
| AppendChatBoxMessage
| AppendReviewCommentsMessage
| TriggerTaskMessage
| VariantsLoadedMessage
| CloudSessionDataLoadedMessage
| CloudSessionImportedMessage
| CloudSessionImportFailedMessage
| OpenCloudSessionMessage
| AgentManagerBranchesMessage
| AgentManagerExternalWorktreesMessage
| AgentManagerImportResultMessage
| WorkspaceDirectoryChangedMessage
| AgentManagerWorktreeDiffMessage
| AgentManagerWorktreeDiffFileMessage
| AgentManagerWorktreeDiffLoadingMessage
| AgentManagerApplyWorktreeDiffResultMessage
| AgentManagerRevertWorktreeFileResultMessage
| AgentManagerWorktreeStatsMessage
| AgentManagerLocalStatsMessage
| AgentManagerPRStatusMessage
| AgentManagerTerminalCreatedMessage
| AgentManagerTerminalClosedMessage
| AgentManagerTerminalErrorMessage
// legacy-migration start
| MigrationStateMessage
| LegacyMigrationDataMessage
| LegacyMigrationProgressMessage
| LegacyMigrationSessionProgressMessage
| LegacyMigrationCompleteMessage
// legacy-migration end
| EnhancePromptResultMessage
| EnhancePromptErrorMessage
| ViewSubAgentSessionMessage
| DiffViewerDiffsMessage
| DiffViewerLoadingMessage
| DiffViewerRevertFileResultMessage
| MarketplaceDataMessage
| MarketplaceInstallResultMessage
| MarketplaceRemoveResultMessage
| ProviderOAuthReadyMessage
| ProviderConnectedMessage
| ProviderDisconnectedMessage
| ProviderActionErrorMessage
| CustomProviderModelsFetchedMessage
| RecentsLoadedMessage
| FavoritesLoadedMessage
| ModelSelectionsLoadedMessage
| LanguageChangedMessage
| ContinueInWorktreeProgressMessage
| WorktreeStatsLoadedMessage
| McpStatusLoadedMessage
| ClearPendingPromptsMessage
| ExtensionDataReadyMessage
| RemoteStatusMessage
@@ -0,0 +1,17 @@
/**
* Types for extension <-> webview message communication
*/
export * from "./connection"
export * from "./parts"
export * from "./sessions"
export * from "./permissions"
export * from "./questions"
export * from "./providers"
export * from "./agents"
export * from "./config"
export * from "./profile"
export * from "./agent-manager"
export * from "./migration"
export * from "./extension-messages"
export * from "./webview-messages"
@@ -0,0 +1,150 @@
// legacy-migration start
export interface MigrationProviderInfo {
profileName: string
provider: string
model?: string
hasApiKey: boolean
supported: boolean
newProviderName?: string
}
export interface MigrationMcpServerInfo {
name: string
type: string
}
export interface MigrationCustomModeInfo {
name: string
slug: string
}
export interface LegacyAutocompleteSettings {
enableAutoTrigger?: boolean
enableSmartInlineTaskKeybinding?: boolean
enableChatAutocomplete?: boolean
}
export interface LegacySettings {
autoApprovalEnabled?: boolean
allowedCommands?: string[]
deniedCommands?: string[]
// Fine-grained auto-approval (legacy globalState keys — no prefix)
alwaysAllowReadOnly?: boolean
alwaysAllowReadOnlyOutsideWorkspace?: boolean
alwaysAllowWrite?: boolean
alwaysAllowExecute?: boolean
alwaysAllowMcp?: boolean
alwaysAllowModeSwitch?: boolean
alwaysAllowSubtasks?: boolean
language?: string
autocomplete?: LegacyAutocompleteSettings
}
export interface MigrationSessionInfo {
id: string
title: string
directory: string
time: number
}
export interface MigrationResultItem {
item: string
category: "provider" | "mcpServer" | "customMode" | "session" | "defaultModel" | "settings"
status: "success" | "warning" | "error"
message?: string
}
export interface MigrationStateMessage {
type: "migrationState"
needed: boolean
data?: {
providers: MigrationProviderInfo[]
mcpServers: MigrationMcpServerInfo[]
customModes: MigrationCustomModeInfo[]
sessions?: MigrationSessionInfo[]
defaultModel?: { provider: string; model: string }
settings?: LegacySettings
}
}
export interface LegacyMigrationDataMessage {
type: "legacyMigrationData"
data: {
providers: MigrationProviderInfo[]
mcpServers: MigrationMcpServerInfo[]
customModes: MigrationCustomModeInfo[]
sessions?: MigrationSessionInfo[]
defaultModel?: { provider: string; model: string }
settings?: LegacySettings
}
}
export interface LegacyMigrationProgressMessage {
type: "legacyMigrationProgress"
item: string
status: "migrating" | "success" | "warning" | "error"
message?: string
}
export type LegacyMigrationSessionPhase = "preparing" | "storing" | "skipped" | "done" | "summary" | "error"
export interface LegacyMigrationSessionProgressMessage {
type: "legacyMigrationSessionProgress"
session: MigrationSessionInfo
index: number
total: number
phase: LegacyMigrationSessionPhase
error?: string
}
export interface LegacyMigrationCompleteMessage {
type: "legacyMigrationComplete"
results: MigrationResultItem[]
}
export interface RequestLegacyMigrationDataMessage {
type: "requestLegacyMigrationData"
}
export interface MigrationAutoApprovalSelections {
commandRules: boolean
readPermission: boolean
writePermission: boolean
executePermission: boolean
mcpPermission: boolean
taskPermission: boolean
}
export interface MigrationSessionSelection {
id: string
force?: boolean
}
export interface StartLegacyMigrationMessage {
type: "startLegacyMigration"
selections: {
providers: string[]
mcpServers: string[]
customModes: string[]
sessions?: MigrationSessionSelection[]
defaultModel: boolean
settings: {
autoApproval: MigrationAutoApprovalSelections
language: boolean
autocomplete: boolean
}
}
}
export interface SkipLegacyMigrationMessage {
type: "skipLegacyMigration"
}
export interface ClearLegacyDataMessage {
type: "clearLegacyData"
}
export interface FinalizeLegacyMigrationMessage {
type: "finalizeLegacyMigration"
}
// legacy-migration end
@@ -0,0 +1,94 @@
// Tool state for tool parts
export type ToolState =
| { status: "pending"; input: Record<string, unknown> }
| { status: "running"; input: Record<string, unknown>; title?: string }
| { status: "completed"; input: Record<string, unknown>; output: string; title: string }
| { status: "error"; input: Record<string, unknown>; error: string }
// Base part interface - all parts have these fields
export interface BasePart {
id: string
sessionID?: string
messageID?: string
}
// Part types from the backend
export interface TextPart extends BasePart {
type: "text"
text: string
}
export interface FilePartSource {
type: "file"
path: string
text: {
value: string
start: number
end: number
}
}
export interface FilePart extends BasePart {
type: "file"
mime: string
url: string
filename?: string
source?: FilePartSource
}
export interface ToolPart extends BasePart {
type: "tool"
tool: string
state: ToolState
}
export interface ReasoningPart extends BasePart {
type: "reasoning"
text: string
}
// Step parts from the backend
export interface StepStartPart extends BasePart {
type: "step-start"
}
export interface StepFinishPart extends BasePart {
type: "step-finish"
reason?: string
cost?: number
tokens?: {
input: number
output: number
reasoning?: number
cache?: { read: number; write: number }
}
}
export type Part = TextPart | FilePart | ToolPart | ReasoningPart | StepStartPart | StepFinishPart
// Part delta for streaming updates
export interface PartDelta {
type: "text-delta"
textDelta?: string
}
// Token usage for assistant messages
export interface TokenUsage {
input: number
output: number
reasoning?: number
cache?: { read: number; write: number }
}
// Context usage derived from the last assistant message's tokens
export interface ContextUsage {
tokens: number
percentage: number | null
}
export interface FileAttachment {
mime: string
url: string
filename?: string
source?: FilePartSource
}
@@ -0,0 +1,39 @@
export type PermissionLevel = "allow" | "ask" | "deny"
/** null in a PermissionRule object is a delete sentinel — removes the key from the config */
export type PermissionRule = PermissionLevel | Record<string, PermissionLevel | null>
export type PermissionConfig = Partial<Record<string, PermissionRule>>
// A single resolved permission rule from the CLI backend (matches PermissionNext.Rule)
export interface PermissionRuleItem {
permission: string
pattern: string
action: PermissionLevel
}
// Permission request
export interface PermissionFileDiff {
file: string
patch?: string
before?: string
after?: string
additions: number
deletions: number
}
export interface PermissionRequest {
id: string
sessionID: string
toolName: string
patterns: string[]
always: string[]
args: Record<string, unknown> & {
rules?: string[]
diff?: string
filepath?: string
filediff?: PermissionFileDiff
}
message?: string
tool?: { messageID: string; callID: string }
}
@@ -0,0 +1,29 @@
// Kilo notification types (mirrored from kilo-gateway)
export interface KilocodeNotificationAction {
actionText: string
actionURL: string
}
export interface KilocodeNotification {
id: string
title: string
message: string
action?: KilocodeNotificationAction
showIn?: string[]
suggestModelId?: string
}
// Profile types from kilo-gateway
export interface KilocodeBalance {
balance: number
}
export interface ProfileData {
profile: {
email: string
name?: string
organizations?: Array<{ id: string; name: string; role: string }>
}
balance: KilocodeBalance | null
currentOrgId: string | null
}
@@ -0,0 +1,54 @@
// Provider/model types for model selector
export interface ProviderModel {
id: string
name: string
inputPrice?: number
outputPrice?: number
contextLength?: number
releaseDate?: string
latest?: boolean
// Actual shape returned by the server (Provider.Model)
limit?: { context: number; input?: number; output: number }
variants?: Record<string, Record<string, unknown>>
capabilities?: {
reasoning: boolean
input?: { text: boolean; image: boolean; audio: boolean; video: boolean; pdf: boolean }
}
options?: { description?: string }
recommendedIndex?: number
isFree?: boolean
cost?: {
input: number
output: number
cache?: {
read: number
write: number
}
}
}
export interface Provider {
id: string
name: string
models: Record<string, ProviderModel>
source?: "env" | "config" | "custom" | "api"
env?: string[]
}
export interface ModelSelection {
providerID: string
modelID: string
}
export type ProviderAuthState = "api" | "oauth" | "wellknown"
export interface ProviderConfig {
name?: string
api_key?: string
base_url?: string
models?: Record<string, unknown>
npm?: string
env?: string[]
options?: Record<string, unknown>
}
@@ -0,0 +1,57 @@
// Todo item
export interface TodoItem {
id: string
content: string
status: "pending" | "in_progress" | "completed"
}
// Question types
export interface QuestionOption {
label: string
description: string
mode?: string
// Optional i18n keys — the backend fills these for strings it wants translated in the webview.
// The canonical English `label` stays on the reply wire, so server-side matching is unaffected.
labelKey?: string
descriptionKey?: string
}
export interface QuestionInfo {
question: string
header: string
options: QuestionOption[]
multiple?: boolean
custom?: boolean
// Optional i18n keys for question text and header (see QuestionOption for details).
questionKey?: string
headerKey?: string
}
export interface QuestionRequest {
id: string
sessionID: string
questions: QuestionInfo[]
blocking?: boolean
tool?: {
messageID: string
callID: string
}
}
export interface SuggestionAction {
label: string
description?: string
prompt: string
}
export interface SuggestionRequest {
id: string
sessionID: string
text: string
actions: SuggestionAction[]
blocking?: boolean
tool?: {
messageID: string
callID: string
}
}
@@ -0,0 +1,65 @@
import type { Part, TokenUsage } from "./parts"
// Message structure (simplified for webview)
export interface Message {
id: string
sessionID: string
role: "user" | "assistant"
content?: string
parts?: Part[]
createdAt: string
time?: { created: number; completed?: number }
agent?: string
model?: { providerID: string; modelID: string }
providerID?: string
modelID?: string
mode?: string
parentID?: string
path?: { cwd: string; root: string }
error?: { name: string; data?: Record<string, unknown> }
summary?: { title?: string; body?: string; diffs?: unknown[] } | boolean
cost?: number
tokens?: TokenUsage
finish?: string
}
// File diff info (matches Snapshot.FileDiff from CLI backend)
export interface SessionFileDiff {
file: string
before: string
after: string
additions: number
deletions: number
status?: "added" | "deleted" | "modified"
}
// Session info (simplified for webview)
export interface SessionInfo {
id: string
parentID?: string | null
title?: string
createdAt: string
updatedAt: string
revert?: {
messageID: string
partID?: string
snapshot?: string
diff?: string
} | null
summary?: {
additions: number
deletions: number
files: number
diffs?: SessionFileDiff[]
} | null
}
// Cloud session info (from Kilo cloud API)
export interface CloudSessionInfo {
session_id: string
title: string | null
created_at: string
updated_at: string
}
export type MessageLoadMode = "replace" | "prepend" | "focus" | "reconcile"
File diff suppressed because it is too large Load Diff
+28
View File
@@ -1,5 +1,33 @@
# @kilocode/cli
## 7.2.25
### Patch Changes
- [#9526](https://github.com/Kilo-Org/kilocode/pull/9526) [`c8113f2`](https://github.com/Kilo-Org/kilocode/commit/c8113f27b190f5c08ce642da57d68646132e1828) - Fix multi-turn DeepSeek reasoning round-tripping on OpenRouter by bumping `@openrouter/ai-sdk-provider` to 2.8.1 in both the CLI and Kilo Gateway packages and letting the SDK handle reasoning details, plus pulling in upstream DeepSeek variant, reasoning-effort, and assistant-reasoning fixes. New DeepSeek conversations are fixed; existing sessions that already stored empty reasoning metadata may still need to be restarted.
- Updated dependencies [[`c8113f2`](https://github.com/Kilo-Org/kilocode/commit/c8113f27b190f5c08ce642da57d68646132e1828)]:
- @kilocode/kilo-gateway@7.2.25
- @kilocode/kilo-telemetry@7.2.25
## 7.2.23
### Minor Changes
- [#9418](https://github.com/Kilo-Org/kilocode/pull/9418) [`12c2d86`](https://github.com/Kilo-Org/kilocode/commit/12c2d86c84ecfce118ffb5b4db7ed4155bbca8fc) - Show the open GitHub PR for the current branch in the session sidebar.
### Patch Changes
- [#9470](https://github.com/Kilo-Org/kilocode/pull/9470) [`7fe4508`](https://github.com/Kilo-Org/kilocode/commit/7fe4508eecf7e7da8336f75c0884d1b310af6c6e) - Fix multi-turn tool calls with DeepSeek thinking mode by preserving empty `reasoning_content` in the interleaved transform.
## 7.2.22
### Patch Changes
- [#9455](https://github.com/Kilo-Org/kilocode/pull/9455) [`567ca0d`](https://github.com/Kilo-Org/kilocode/commit/567ca0d34178a6a896aa58c10cc946565c116d4e) - Fix a 1-2 second startup delay before home content (agents, news, tips) appears in the TUI.
- [#9425](https://github.com/Kilo-Org/kilocode/pull/9425) [`6ee160f`](https://github.com/Kilo-Org/kilocode/commit/6ee160f89c10293d635990798779988d34b092b4) - Preserve typed text in the main prompt when a blocking question, suggestion, permission, or network overlay is shown and then dismissed.
## 7.2.21
### Minor Changes

Some files were not shown because too many files have changed in this diff Show More