mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-31 01:37:28 +08:00
Merge branch 'main' into fix/costs-of-subagents
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"@kilocode/kilo-gateway": patch
|
||||
---
|
||||
|
||||
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.
|
||||
Generated
+271
-10
@@ -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",
|
||||
|
||||
Generated
+271
-10
@@ -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",
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
@@ -0,0 +1,261 @@
|
||||
# TESTING.md
|
||||
|
||||
How to spin up the **local main-branch** Kilo backend and test it with `curl` / `fetch`. Aimed at a running Kilo CLI agent iterating on backend fixes without rebuilding the VS Code extension, TUI, or desktop app.
|
||||
|
||||
All examples use plain shell + `curl`. Writing TypeScript files is a last resort (see Section 8).
|
||||
|
||||
## TL;DR
|
||||
|
||||
```bash
|
||||
# From repo root. Starts the LOCAL main-branch backend in the background.
|
||||
PASS=$(openssl rand -hex 16)
|
||||
KILO_SERVER_PASSWORD="$PASS" bun dev serve --port 0 >/tmp/kilo-serve.log 2>&1 &
|
||||
echo $! >/tmp/kilo-serve.pid
|
||||
while ! grep -q "kilo server listening" /tmp/kilo-serve.log 2>/dev/null; do sleep 0.1; done
|
||||
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/kilo-serve.log | grep -oE "[0-9]+$")
|
||||
AUTH="Authorization: Basic $(printf 'kilo:%s' "$PASS" | base64 | tr -d '\n')"
|
||||
BASE="http://127.0.0.1:$PORT"
|
||||
|
||||
# Call any endpoint
|
||||
curl -sS -H "$AUTH" -H "x-kilo-directory: $PWD" "$BASE/global/health"
|
||||
|
||||
# Stop when done
|
||||
kill "$(cat /tmp/kilo-serve.pid)" 2>/dev/null || true
|
||||
rm -f /tmp/kilo-serve.pid /tmp/kilo-serve.log
|
||||
```
|
||||
|
||||
## 1. What this doc is for
|
||||
|
||||
Testing local backend fixes against a real running server, talking to it over HTTP the same way the VS Code extension, TUI, and `kilo run --attach` do — but without any of those clients. Every request is a `curl` the agent can copy-paste.
|
||||
|
||||
For in-process tests (no socket, fastest loop) see `packages/opencode/test/kilocode/server/permission-allow-everything.test.ts` for the `Server.Default().app.request(...)` pattern. That's the right tool inside the `packages/opencode/` test suite; this doc is for out-of-process HTTP testing.
|
||||
|
||||
## 2. `kilo serve` vs `bun dev serve` — important
|
||||
|
||||
| Command | What it runs |
|
||||
|---|---|
|
||||
| `kilo serve` | The npm-installed production CLI on `$PATH`. **Not the code in this repo.** |
|
||||
| `bun dev serve …` (repo root) | The local main-branch backend from this worktree. **This is what you want.** |
|
||||
| `bun run --cwd packages/opencode --conditions=browser src/index.ts serve …` | Same as `bun dev serve`, fully expanded. |
|
||||
|
||||
Root `package.json` defines `"dev"` as the full `bun run --cwd packages/opencode --conditions=browser src/index.ts` invocation, so `bun dev <args>` forwards `<args>` to the local CLI entry point (`packages/opencode/src/index.ts`) without touching the installed binary.
|
||||
|
||||
`bun dev` imports the source directly — no rebuild is needed between code edits. Just kill the running server and relaunch.
|
||||
|
||||
Do **not** use `createKiloServer()` from `@kilocode/sdk/v2` to test local code: it spawns the PATH `kilo` binary (`packages/sdk/js/src/v2/server.ts:38-136`), which is the wrong tool here.
|
||||
|
||||
## 3. Starting the backend (background)
|
||||
|
||||
### Random port (recommended)
|
||||
|
||||
```bash
|
||||
KILO_SERVER_PASSWORD=$(openssl rand -hex 16) \
|
||||
bun dev serve --port 0 >/tmp/kilo-serve.log 2>&1 &
|
||||
echo $! >/tmp/kilo-serve.pid
|
||||
while ! grep -q "kilo server listening" /tmp/kilo-serve.log; do sleep 0.1; done
|
||||
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/kilo-serve.log | grep -oE "[0-9]+$")
|
||||
```
|
||||
|
||||
### Fixed port (if you need a stable URL)
|
||||
|
||||
```bash
|
||||
KILO_SERVER_PASSWORD=secret \
|
||||
bun dev serve --port 4096 --hostname 127.0.0.1 \
|
||||
>/tmp/kilo-serve.log 2>&1 &
|
||||
echo $! >/tmp/kilo-serve.pid
|
||||
while ! grep -q "kilo server listening" /tmp/kilo-serve.log; do sleep 0.1; done
|
||||
PORT=4096
|
||||
```
|
||||
|
||||
### No-auth quickstart (fastest)
|
||||
|
||||
Omit `KILO_SERVER_PASSWORD` entirely. The server prints `Warning: KILO_SERVER_PASSWORD is not set; server is unsecured.` and the auth middleware is bypassed — fine for throwaway local testing, never for anything else.
|
||||
|
||||
```bash
|
||||
bun dev serve --port 0 >/tmp/kilo-serve.log 2>&1 &
|
||||
echo $! >/tmp/kilo-serve.pid
|
||||
while ! grep -q "kilo server listening" /tmp/kilo-serve.log; do sleep 0.1; done
|
||||
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/kilo-serve.log | grep -oE "[0-9]+$")
|
||||
BASE="http://127.0.0.1:$PORT"
|
||||
# no AUTH var needed
|
||||
```
|
||||
|
||||
### Flags (`packages/opencode/src/cli/network.ts`)
|
||||
|
||||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--port` | `0` (OS-assigned) | Must be passed literally when overriding `opencode.json`'s `server.port`. |
|
||||
| `--hostname` | `127.0.0.1` | Becomes `0.0.0.0` when `--mdns` is set without an override. |
|
||||
| `--mdns` | `false` | Publishes an mDNS SRV record. |
|
||||
| `--mdns-domain` | `kilo.local` | |
|
||||
| `--cors` | `[]` | Extra allowed origins. |
|
||||
|
||||
## 4. The two mandatory request knobs
|
||||
|
||||
### Auth header (only if `KILO_SERVER_PASSWORD` was set)
|
||||
|
||||
```bash
|
||||
AUTH="Authorization: Basic $(printf 'kilo:%s' "$KILO_SERVER_PASSWORD" | base64 | tr -d '\n')"
|
||||
```
|
||||
|
||||
The username is literally `kilo` (same as the VS Code extension). Skip this whole block if you launched without a password.
|
||||
|
||||
### Directory header on every call
|
||||
|
||||
```bash
|
||||
DIR_HEADER="x-kilo-directory: $PWD"
|
||||
```
|
||||
|
||||
`InstanceMiddleware` uses this to scope the request to a project. For `GET` / `HEAD`, pass `?directory=<urlencoded>` in the URL instead — that's what the SDK does internally.
|
||||
|
||||
## 5. Common `curl` recipes
|
||||
|
||||
All of the below assume `BASE`, `AUTH`, `DIR_HEADER` are set. Drop `-H "$AUTH"` if you're running without a password.
|
||||
|
||||
### Health check (no auth, no directory)
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE/global/health"
|
||||
```
|
||||
|
||||
### Full endpoint list (OpenAPI spec)
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE/doc" | jq .
|
||||
```
|
||||
|
||||
This is the source of truth — anything not in this doc is discoverable from `/doc` without reading source.
|
||||
|
||||
### Create a session
|
||||
|
||||
```bash
|
||||
SID=$(curl -sS -X POST "$BASE/session" \
|
||||
-H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
|
||||
-d '{}' | jq -r .id)
|
||||
echo "$SID"
|
||||
```
|
||||
|
||||
### List sessions (GET — directory goes in the query)
|
||||
|
||||
```bash
|
||||
curl -sS -H "$AUTH" \
|
||||
"$BASE/session?directory=$(printf %s "$PWD" | jq -sRr @uri)"
|
||||
```
|
||||
|
||||
### Send a message (fire-and-forget)
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "$BASE/session/$SID/prompt_async" \
|
||||
-H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
|
||||
-d '{"parts":[{"type":"text","text":"hello"}]}'
|
||||
```
|
||||
|
||||
### Read messages for a session
|
||||
|
||||
```bash
|
||||
curl -sS -H "$AUTH" \
|
||||
"$BASE/session/$SID/message?directory=$(printf %s "$PWD" | jq -sRr @uri)"
|
||||
```
|
||||
|
||||
### Abort an in-flight prompt
|
||||
|
||||
```bash
|
||||
curl -sS -X POST -H "$AUTH" -H "$DIR_HEADER" "$BASE/session/$SID/abort"
|
||||
```
|
||||
|
||||
### Get the resolved config (verify a config change took effect)
|
||||
|
||||
```bash
|
||||
curl -sS -H "$AUTH" \
|
||||
"$BASE/config?directory=$(printf %s "$PWD" | jq -sRr @uri)"
|
||||
```
|
||||
|
||||
### Stream global events (SSE)
|
||||
|
||||
```bash
|
||||
curl -N -sS -H "$AUTH" \
|
||||
"$BASE/global/event?directory=$(printf %s "$PWD" | jq -sRr @uri)"
|
||||
```
|
||||
|
||||
`-N` disables curl's output buffering so events appear live. Expect lines like `data: {"directory":"…","payload":{"type":"…",…}}`.
|
||||
|
||||
## 6. Stopping the backend
|
||||
|
||||
```bash
|
||||
kill "$(cat /tmp/kilo-serve.pid)" 2>/dev/null || true
|
||||
rm -f /tmp/kilo-serve.pid /tmp/kilo-serve.log
|
||||
```
|
||||
|
||||
`ServeCommand` handles `SIGTERM` / `SIGINT` / `SIGHUP` and runs `Instance.disposeAll()` + `server.stop(true)` before exiting (`packages/opencode/src/cli/cmd/serve.ts:29-31`). `-9` is only needed if the process hangs past ~5 s.
|
||||
|
||||
## 7. Useful environment variables
|
||||
|
||||
| Var | Why you'd set it |
|
||||
|---|---|
|
||||
| `KILO_SERVER_PASSWORD` | Enable Basic auth. Omit for auth-bypassed local testing. |
|
||||
| `KILO_DB=":memory:"` | Skip on-disk SQLite — hermetic runs. |
|
||||
| `KILO_DISABLE_DEFAULT_PLUGINS=true` | Don't auto-load bundled plugins. |
|
||||
| `KILO_WORKSPACE_ID=<id>` | Single-workspace mode; disables control-plane routes. |
|
||||
| `KILO_TELEMETRY_LEVEL=off` | Disable PostHog during tests. |
|
||||
| `KILO_CONFIG_CONTENT='{…}'` | Inline JSON config without writing a file. |
|
||||
|
||||
## 8. Last resort: typed SDK via a throwaway script
|
||||
|
||||
Use this only when `curl` can't express what you need — typed request/response shapes, complex multi-turn orchestration, SSE consumers that need to coalesce events. Keep the file short and delete it after.
|
||||
|
||||
```ts
|
||||
// /tmp/probe.ts — delete after use. Talks to an already-running backend.
|
||||
import { createKiloClient } from "@kilocode/sdk/v2"
|
||||
|
||||
const port = process.env.PORT!
|
||||
const pass = process.env.KILO_SERVER_PASSWORD
|
||||
const headers = pass
|
||||
? { Authorization: "Basic " + Buffer.from("kilo:" + pass).toString("base64") }
|
||||
: undefined
|
||||
|
||||
const client = createKiloClient({
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
headers,
|
||||
directory: process.cwd(),
|
||||
})
|
||||
|
||||
const { data: session } = await client.session.create({}, { throwOnError: true })
|
||||
await client.session.promptAsync({
|
||||
sessionID: session.id,
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
})
|
||||
|
||||
const events = await client.global.event({})
|
||||
for await (const ev of events.stream) {
|
||||
const e = ev as { payload: { type: string } }
|
||||
console.log(e.payload.type)
|
||||
if (e.payload.type === "session.idle") break
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
PORT="$PORT" KILO_SERVER_PASSWORD="$KILO_SERVER_PASSWORD" bun /tmp/probe.ts
|
||||
rm /tmp/probe.ts
|
||||
```
|
||||
|
||||
Reminder: this script **connects to** the server you launched in Section 3 — it does not start one. `createKiloServer()` from the SDK would spawn the PATH `kilo` binary (production CLI), which defeats the point of testing local code.
|
||||
|
||||
## 9. Pitfalls
|
||||
|
||||
- Running `kilo serve` instead of `bun dev serve` runs the installed prod binary, not your edits.
|
||||
- Missing `x-kilo-directory` (or `?directory=`) returns `400` from `InstanceMiddleware`.
|
||||
- `curl` without `-N` buffers SSE output — you won't see events until the connection closes.
|
||||
- Hardcoding port `4096` breaks when a previous run didn't exit cleanly. Parse the log instead.
|
||||
- `--port` must appear literally in `argv` to override `opencode.json`'s `server.port` (`packages/opencode/src/cli/network.ts:45`).
|
||||
- `KILO_SERVER_PASSWORD` must be set **before** launch — changing it after doesn't rotate credentials.
|
||||
- When sharing `/tmp/kilo-serve.log` / `.pid` across terminals, unique-suffix the paths to avoid clobbering parallel runs.
|
||||
|
||||
## 10. After changing server routes
|
||||
|
||||
Regenerate the SDK and OpenAPI spec so `/doc` and typed clients stay in sync:
|
||||
|
||||
```bash
|
||||
./script/generate.ts # from repo root
|
||||
```
|
||||
|
||||
See `AGENTS.md` for the full rationale.
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"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.24",
|
||||
"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.24",
|
||||
"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.24",
|
||||
"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.24",
|
||||
"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.24",
|
||||
"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.24",
|
||||
"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.24",
|
||||
"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.24",
|
||||
"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.24",
|
||||
"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.24",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -545,7 +545,7 @@
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"dependencies": {
|
||||
"semver": "^7.6.3",
|
||||
},
|
||||
@@ -556,7 +556,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -571,7 +571,7 @@
|
||||
},
|
||||
"packages/shared": {
|
||||
"name": "@opencode-ai/shared",
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -595,7 +595,7 @@
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"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.24",
|
||||
"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
@@ -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
@@ -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.24",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
Generated
+49
-49
@@ -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",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.2.21"
|
||||
version = "7.2.24"
|
||||
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.24/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.24/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.24/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.24/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.24/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -33,6 +33,11 @@ export const AutomateNav: NavSection[] = [
|
||||
href: "/automate/extending/shell-integration",
|
||||
children: "Shell Integration",
|
||||
},
|
||||
{
|
||||
href: "/automate/extending/plugins",
|
||||
children: "Plugins",
|
||||
platform: "new",
|
||||
},
|
||||
{
|
||||
href: "/automate/extending/auto-launch",
|
||||
children: "Auto-launch Configuration",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"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,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 -->
|
||||
@@ -124,6 +124,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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"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,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Kilo-specific i18n translations and overrides",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# kilo-code
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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.24",
|
||||
"icon": "assets/icons/logo-outline-black.png",
|
||||
"galleryBanner": {
|
||||
"color": "#FFFFFF",
|
||||
|
||||
@@ -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"
|
||||
@@ -565,17 +567,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
|
||||
@@ -3322,6 +3321,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,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,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,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}>
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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
@@ -1,5 +1,23 @@
|
||||
# @kilocode/cli
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.2.21",
|
||||
"version": "7.2.24",
|
||||
"name": "@kilocode/cli",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -86,7 +86,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",
|
||||
@@ -105,7 +105,7 @@
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@gitlab/gitlab-ai-provider": "3.6.0",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||
"@hono/node-server": "1.19.11",
|
||||
"@hono/node-server": "1.19.13",
|
||||
"@hono/node-ws": "1.3.0",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
@@ -121,7 +121,7 @@
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||
"@openrouter/ai-sdk-provider": "2.8.1",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/context-async-hooks": "2.6.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||
@@ -148,7 +148,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",
|
||||
|
||||
@@ -7,6 +7,22 @@ import { fileURLToPath } from "url"
|
||||
const dir = fileURLToPath(new URL("..", import.meta.url))
|
||||
process.chdir(dir)
|
||||
|
||||
async function published(name: string, version: string) {
|
||||
return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0
|
||||
}
|
||||
|
||||
async function publish(dir: string, name: string, version: string) {
|
||||
// GitHub artifact downloads can drop the executable bit, and Docker uses the
|
||||
// unpacked dist binaries directly rather than the published tarball.
|
||||
if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir)
|
||||
if (await published(name, version)) {
|
||||
console.log(`already published ${name}@${version}`)
|
||||
return
|
||||
}
|
||||
await $`bun pm pack`.cwd(dir)
|
||||
await $`npm publish *.tgz --access public --tag ${Script.channel} --provenance`.cwd(dir) // kilocode_change
|
||||
}
|
||||
|
||||
const binaries: Record<string, string> = {}
|
||||
// kilocode_change start
|
||||
for (const filepath of new Bun.Glob("*/*/package.json").scanSync({ cwd: "./dist" })) {
|
||||
@@ -52,14 +68,10 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||
)
|
||||
|
||||
const tasks = Object.entries(binaries).map(async ([name]) => {
|
||||
if (process.platform !== "win32") {
|
||||
await $`chmod -R 755 .`.cwd(`./dist/${name}`)
|
||||
}
|
||||
await $`bun pm pack`.cwd(`./dist/${name}`)
|
||||
await $`npm publish *.tgz --access public --tag ${Script.channel} --provenance`.cwd(`./dist/${name}`) // kilocode_change
|
||||
await publish(`./dist/${name}`, name, binaries[name])
|
||||
})
|
||||
await Promise.all(tasks)
|
||||
await $`cd ./dist/${pkg.name} && bun pm pack && npm publish *.tgz --access public --tag ${Script.channel} --provenance` // kilocode_change
|
||||
await publish(`./dist/${pkg.name}`, pkg.name, version) // kilocode_change
|
||||
|
||||
const image = "ghcr.io/kilo-org/kilo" // kilocode_change
|
||||
const platforms = "linux/amd64,linux/arm64"
|
||||
@@ -117,6 +129,7 @@ if (!Script.preview) {
|
||||
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
|
||||
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
|
||||
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
|
||||
if ((await $`cd ./dist/aur-${pkg} && git diff --cached --quiet`.nothrow()).exitCode === 0) break
|
||||
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${Script.version}"`
|
||||
await $`cd ./dist/aur-${pkg} && git push`
|
||||
break
|
||||
@@ -189,6 +202,8 @@ if (!Script.preview) {
|
||||
await $`git clone ${tap} ./dist/homebrew-tap`
|
||||
await Bun.file("./dist/homebrew-tap/kilo.rb").write(homebrewFormula) // kilocode_change
|
||||
await $`cd ./dist/homebrew-tap && git add kilo.rb` // kilocode_change
|
||||
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
|
||||
await $`cd ./dist/homebrew-tap && git push`
|
||||
if ((await $`cd ./dist/homebrew-tap && git diff --cached --quiet`.nothrow()).exitCode !== 0) {
|
||||
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
|
||||
await $`cd ./dist/homebrew-tap && git push`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# Facade removal checklist
|
||||
|
||||
Concrete inventory of the remaining `makeRuntime(...)`-backed service facades in `packages/opencode`.
|
||||
Concrete inventory of the remaining `makeRuntime(...)`-backed facades in `packages/opencode`.
|
||||
|
||||
As of 2026-04-13, latest `origin/dev`:
|
||||
Current status on this branch:
|
||||
|
||||
- `src/` still has 15 `makeRuntime(...)` call sites.
|
||||
- 13 of those are still in scope for facade removal.
|
||||
- 2 are excluded from this checklist: `bus/index.ts` and `effect/cross-spawn-spawner.ts`.
|
||||
- `src/` has 5 `makeRuntime(...)` call sites total.
|
||||
- 2 are intentionally excluded from this checklist: `src/bus/index.ts` and `src/effect/cross-spawn-spawner.ts`.
|
||||
- 1 is tracked primarily by the instance-context migration rather than facade removal: `src/project/instance.ts`.
|
||||
- That leaves 2 live runtime-backed service facades still worth tracking here: `src/npm/index.ts` and `src/cli/cmd/tui/config/tui.ts`.
|
||||
|
||||
Recent progress:
|
||||
|
||||
@@ -15,8 +16,9 @@ Recent progress:
|
||||
|
||||
## Priority hotspots
|
||||
|
||||
- `server/instance/session.ts` still depends on `Session`, `SessionPrompt`, `SessionRevert`, `SessionCompaction`, `SessionSummary`, `ShareSession`, `Agent`, and `Permission` facades.
|
||||
- `src/effect/app-runtime.ts` still references many facade namespaces directly, so it should stay in view during each deletion.
|
||||
- `src/cli/cmd/tui/config/tui.ts` still exports `makeRuntime(...)` plus async facade helpers for `get()` and `waitForDependencies()`.
|
||||
- `src/npm/index.ts` still exports `makeRuntime(...)` plus async facade helpers for `install()`, `add()`, `outdated()`, and `which()`.
|
||||
- `src/project/instance.ts` still uses a dedicated runtime for project boot, but that file is really part of the broader legacy instance-context transition tracked in `instance-context.md`.
|
||||
|
||||
## Completed Batches
|
||||
|
||||
@@ -184,53 +186,34 @@ These were the recurring mistakes and useful corrections from the first two batc
|
||||
5. For CLI readability, extract file-local preload helpers when the handler starts doing config load + service load + batched effect fanout inline.
|
||||
6. When rebasing a facade branch after nearby merges, prefer the already-cleaned service/test version over older inline facade-era code.
|
||||
|
||||
## Next batch
|
||||
## Remaining work
|
||||
|
||||
Recommended next five, in order:
|
||||
Most of the original facade-removal backlog is already done. The practical remaining work is narrower now:
|
||||
|
||||
1. `src/permission/index.ts`
|
||||
2. `src/agent/agent.ts`
|
||||
3. `src/session/summary.ts`
|
||||
4. `src/session/revert.ts`
|
||||
5. `src/mcp/auth.ts`
|
||||
|
||||
Why this batch:
|
||||
|
||||
- It keeps pushing the session-adjacent cleanup without jumping straight into `session/index.ts` or `session/prompt.ts`.
|
||||
- `Permission`, `Agent`, `SessionSummary`, and `SessionRevert` all reduce fanout in `server/instance/session.ts`.
|
||||
- `McpAuth` is small and closely related to the just-landed `MCP` cleanup.
|
||||
|
||||
After that batch, the expected follow-up is the main session cluster:
|
||||
|
||||
1. `src/session/index.ts`
|
||||
2. `src/session/prompt.ts`
|
||||
3. `src/session/compaction.ts`
|
||||
1. remove the `Npm` runtime-backed facade from `src/npm/index.ts`
|
||||
2. remove the `TuiConfig` runtime-backed facade from `src/cli/cmd/tui/config/tui.ts`
|
||||
3. keep `src/project/instance.ts` in the separate instance-context migration, not this checklist
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] `src/session/index.ts` (`Session`) - facades: `create`, `fork`, `get`, `setTitle`, `setArchived`, `setPermission`, `setRevert`, `messages`, `children`, `remove`, `updateMessage`, `removeMessage`, `removePart`, `updatePart`; main callers: `server/instance/session.ts`, `cli/cmd/session.ts`, `cli/cmd/export.ts`, `cli/cmd/github.ts`; tests: `test/server/session-actions.test.ts`, `test/server/session-list.test.ts`, `test/server/global-session-list.test.ts`
|
||||
- [ ] `src/session/prompt.ts` (`SessionPrompt`) - facades: `prompt`, `resolvePromptParts`, `cancel`, `loop`, `shell`, `command`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts`; tests: `test/session/prompt.test.ts`, `test/session/prompt-effect.test.ts`, `test/session/structured-output-integration.test.ts`
|
||||
- [ ] `src/session/revert.ts` (`SessionRevert`) - facades: `revert`, `unrevert`, `cleanup`; main callers: `server/instance/session.ts`; tests: `test/session/revert-compact.test.ts`
|
||||
- [ ] `src/session/compaction.ts` (`SessionCompaction`) - facades: `isOverflow`, `prune`, `create`; main callers: `server/instance/session.ts`; tests: `test/session/compaction.test.ts`
|
||||
- [ ] `src/session/summary.ts` (`SessionSummary`) - facades: `summarize`, `diff`; main callers: `session/prompt.ts`, `session/processor.ts`, `server/instance/session.ts`; tests: `test/session/snapshot-tool-race.test.ts`
|
||||
- [ ] `src/share/session.ts` (`ShareSession`) - facades: `create`, `share`, `unshare`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts`
|
||||
- [ ] `src/agent/agent.ts` (`Agent`) - facades: `get`, `list`, `defaultAgent`, `generate`; main callers: `cli/cmd/agent.ts`, `server/instance/session.ts`, `server/instance/experimental.ts`; tests: `test/agent/agent.test.ts`
|
||||
- [ ] `src/permission/index.ts` (`Permission`) - facades: `ask`, `reply`, `list`; main callers: `server/instance/permission.ts`, `server/instance/session.ts`, `session/llm.ts`; tests: `test/permission/next.test.ts`
|
||||
- [x] `src/file/index.ts` (`File`) - facades removed and merged.
|
||||
- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged.
|
||||
- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged.
|
||||
- [x] `src/config/config.ts` (`Config`) - facades removed and merged.
|
||||
- [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged.
|
||||
- [x] `src/pty/index.ts` (`Pty`) - facades removed and merged.
|
||||
- [x] `src/skill/index.ts` (`Skill`) - facades removed and merged.
|
||||
- [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged.
|
||||
- [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged.
|
||||
- [ ] `src/worktree/index.ts` (`Worktree`) - facades: `makeWorktreeInfo`, `createFromInfo`, `create`, `remove`, `reset`; main callers: `control-plane/adaptors/worktree.ts`, `server/instance/experimental.ts`; tests: `test/project/worktree.test.ts`, `test/project/worktree-remove.test.ts`
|
||||
- [x] `src/auth/index.ts` (`Auth`) - facades removed and merged.
|
||||
- [ ] `src/mcp/auth.ts` (`McpAuth`) - facades: `get`, `getForUrl`, `all`, `set`, `remove`, `updateTokens`, `updateClientInfo`, `updateCodeVerifier`, `updateOAuthState`; main callers: `mcp/oauth-provider.ts`, `cli/cmd/mcp.ts`; tests: `test/mcp/oauth-auto-connect.test.ts`
|
||||
- [ ] `src/plugin/index.ts` (`Plugin`) - facades: `trigger`, `list`, `init`; main callers: `agent/agent.ts`, `session/llm.ts`, `project/bootstrap.ts`; tests: `test/plugin/trigger.test.ts`, `test/provider/provider.test.ts`
|
||||
- [ ] `src/project/project.ts` (`Project`) - facades: `fromDirectory`, `discover`, `initGit`, `update`, `sandboxes`, `addSandbox`, `removeSandbox`; main callers: `project/instance.ts`, `server/instance/project.ts`, `server/instance/experimental.ts`; tests: `test/project/project.test.ts`, `test/project/migrate-global.test.ts`
|
||||
- [ ] `src/snapshot/index.ts` (`Snapshot`) - facades: `init`, `track`, `patch`, `restore`, `revert`, `diff`, `diffFull`; main callers: `project/bootstrap.ts`, `cli/cmd/debug/snapshot.ts`; tests: `test/snapshot/snapshot.test.ts`, `test/session/revert-compact.test.ts`
|
||||
- [ ] `src/npm/index.ts` (`Npm`) - still exports runtime-backed async facade helpers on top of `Npm.Service`
|
||||
- [ ] `src/cli/cmd/tui/config/tui.ts` (`TuiConfig`) - still exports runtime-backed async facade helpers on top of `TuiConfig.Service`
|
||||
- [x] `src/session/session.ts` / `src/session/prompt.ts` / `src/session/revert.ts` / `src/session/summary.ts` - service-local facades removed
|
||||
- [x] `src/agent/agent.ts` (`Agent`) - service-local facades removed
|
||||
- [x] `src/permission/index.ts` (`Permission`) - service-local facades removed
|
||||
- [x] `src/worktree/index.ts` (`Worktree`) - service-local facades removed
|
||||
- [x] `src/plugin/index.ts` (`Plugin`) - service-local facades removed
|
||||
- [x] `src/snapshot/index.ts` (`Snapshot`) - service-local facades removed
|
||||
- [x] `src/file/index.ts` (`File`) - facades removed and merged
|
||||
- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged
|
||||
- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged
|
||||
- [x] `src/config/config.ts` (`Config`) - facades removed and merged
|
||||
- [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged
|
||||
- [x] `src/pty/index.ts` (`Pty`) - facades removed and merged
|
||||
- [x] `src/skill/index.ts` (`Skill`) - facades removed and merged
|
||||
- [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged
|
||||
- [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged
|
||||
- [x] `src/auth/index.ts` (`Auth`) - facades removed and merged
|
||||
|
||||
## Excluded `makeRuntime(...)` sites
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ Many route boundaries still use Zod-first validators. That does not block all ex
|
||||
|
||||
### Mixed handler styles
|
||||
|
||||
Many current `server/instance/*.ts` handlers still call async facades directly. Migrating those to composed `Effect.gen(...)` handlers is the low-risk step to do first.
|
||||
Many current `server/routes/instance/*.ts` handlers still mix composed Effect code with smaller Promise- or ALS-backed seams. Migrating those to consistent `Effect.gen(...)` handlers is the low-risk step to do first.
|
||||
|
||||
### Non-JSON routes
|
||||
|
||||
@@ -90,7 +90,7 @@ The current server composition, middleware, and docs flow are Hono-centered toda
|
||||
|
||||
### 1. Finish the prerequisites first
|
||||
|
||||
- continue route-handler effectification in `server/instance/*.ts`
|
||||
- continue route-handler effectification in `server/routes/instance/*.ts`
|
||||
- continue schema migration toward Effect Schema-first DTOs and errors
|
||||
- keep removing service facades
|
||||
|
||||
@@ -98,9 +98,9 @@ The current server composition, middleware, and docs flow are Hono-centered toda
|
||||
|
||||
Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in:
|
||||
|
||||
- `server/instance/question.ts`
|
||||
- `server/instance/provider.ts`
|
||||
- `server/instance/permission.ts`
|
||||
- `server/routes/instance/question.ts`
|
||||
- `server/routes/instance/provider.ts`
|
||||
- `server/routes/instance/permission.ts`
|
||||
|
||||
Avoid `session.ts`, SSE, websocket, and TUI-facing routes first.
|
||||
|
||||
@@ -155,9 +155,9 @@ This gives:
|
||||
|
||||
As each route group is ported to `HttpApi`:
|
||||
|
||||
1. change its `root` path from `/experimental/httpapi/<group>` to `/<group>`
|
||||
2. add `.all("/<group>", handler)` / `.all("/<group>/*", handler)` to the flag block in `instance/index.ts`
|
||||
3. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path
|
||||
1. add `.get(...)` / `.post(...)` bridge entries to the flag block in `server/routes/instance/index.ts`
|
||||
2. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path
|
||||
3. keep the legacy Hono route registered behind it for OpenAPI / SDK generation until the spec pipeline changes
|
||||
4. verify SDK output is unchanged
|
||||
|
||||
Leave streaming-style endpoints on Hono until there is a clear reason to move them.
|
||||
@@ -189,10 +189,46 @@ Ordering for a route-group migration:
|
||||
|
||||
SDK shape rule:
|
||||
|
||||
- every schema migration must preserve the generated SDK output byte-for-byte
|
||||
- `Schema.Class` emits a named `$ref` in OpenAPI via its identifier — use it only for types that already had `.meta({ ref })` in the old Zod schema
|
||||
- inner / nested types that were anonymous in the old Zod schema should stay as `Schema.Struct` (not `Schema.Class`) to avoid introducing new named components in the OpenAPI spec
|
||||
- if a diff appears in `packages/sdk/js/src/v2/gen/types.gen.ts`, the migration introduced an unintended API surface change — fix it before merging
|
||||
- every schema migration must preserve the generated SDK output byte-for-byte **unless the new ref is intentional** (see Schema.Class vs Schema.Struct below)
|
||||
- if an unintended diff appears in `packages/sdk/js/src/v2/gen/types.gen.ts`, the migration introduced an unintended API surface change — fix it before merging
|
||||
|
||||
### Schema.Class vs Schema.Struct
|
||||
|
||||
The pattern choice determines whether a schema becomes a **named** export in the SDK or stays **anonymous inline**.
|
||||
|
||||
**Schema.Class** emits a named `$ref` in OpenAPI via its identifier → produces a named `export type Foo = ...` in `types.gen.ts`:
|
||||
|
||||
```ts
|
||||
export class Info extends Schema.Class<Info>("FooConfig")({ ... }) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
```
|
||||
|
||||
**Schema.Struct** stays anonymous and is inlined everywhere it is referenced:
|
||||
|
||||
```ts
|
||||
export const Info = Schema.Struct({ ... }).pipe(
|
||||
withStatics((s) => ({ zod: zod(s) })),
|
||||
)
|
||||
export type Info = Schema.Schema.Type<typeof Info>
|
||||
```
|
||||
|
||||
When to use each:
|
||||
|
||||
- Use **Schema.Class** when:
|
||||
- the original Zod had `.meta({ ref: ... })` (preserve the existing named SDK type byte-for-byte)
|
||||
- the schema is a top-level endpoint request or response (SDK consumers benefit from a stable importable name)
|
||||
- Use **Schema.Struct** when:
|
||||
- the type is only used as a nested field inside another named schema
|
||||
- the original Zod was anonymous and promoting it would bloat SDK types with no import value
|
||||
|
||||
Promoting a previously-anonymous schema to Schema.Class is acceptable when it is top-level or endpoint-facing, but call it out in the PR — it is an additive SDK change (`export type Foo = ...` newly appears) even if it preserves the JSON shape.
|
||||
|
||||
Schemas that are **not** pure objects (enums, unions, records, tuples) cannot use Schema.Class. For those, add `.annotate({ identifier: "FooName" })` to get the same named-ref behavior:
|
||||
|
||||
```ts
|
||||
export const Action = Schema.Literals(["ask", "allow", "deny"]).annotate({ identifier: "PermissionActionConfig" })
|
||||
```
|
||||
|
||||
Temporary exception:
|
||||
|
||||
@@ -231,7 +267,7 @@ Use the same sequence for each route group.
|
||||
3. Apply the schema migration ordering above so those types are Effect Schema-first.
|
||||
4. Define the `HttpApi` contract separately from the handlers.
|
||||
5. Implement handlers by yielding the existing service from context.
|
||||
6. Mount the new surface in parallel under an experimental prefix.
|
||||
6. Mount the new surface in parallel behind the `KILO_EXPERIMENTAL_HTTPAPI` bridge.
|
||||
7. Regenerate the SDK and verify zero diff against `dev` (see SDK shape rule above).
|
||||
8. Add one end-to-end test and one OpenAPI-focused test.
|
||||
9. Compare ergonomics before migrating the next endpoint.
|
||||
@@ -250,20 +286,20 @@ Placement rule:
|
||||
- keep `HttpApi` code under `src/server`, not `src/effect`
|
||||
- `src/effect` should stay focused on runtimes, layers, instance state, and shared Effect plumbing
|
||||
- place each `HttpApi` slice next to the HTTP boundary it serves
|
||||
- for instance-scoped routes, prefer `src/server/instance/httpapi/*`
|
||||
- if control-plane routes ever migrate, prefer `src/server/control/httpapi/*`
|
||||
- for instance-scoped routes, prefer `src/server/routes/instance/httpapi/*`
|
||||
- if control-plane routes ever migrate, prefer `src/server/routes/control/httpapi/*`
|
||||
|
||||
Suggested file layout for a repeatable spike:
|
||||
|
||||
- `src/server/instance/httpapi/question.ts` — contract and handler layer for one route group
|
||||
- `src/server/instance/httpapi/server.ts` — standalone Effect HTTP server that composes all groups
|
||||
- `test/server/question-httpapi.test.ts` — end-to-end test against the real service
|
||||
- `src/server/routes/instance/httpapi/question.ts` — contract and handler layer for one route group
|
||||
- `src/server/routes/instance/httpapi/server.ts` — bridged Effect HTTP layer that composes all groups
|
||||
- route or OpenAPI verification should live alongside the existing server tests; there is no dedicated `question-httpapi` test file on this branch
|
||||
|
||||
Suggested responsibilities:
|
||||
|
||||
- `question.ts` defines the `HttpApi` contract and `HttpApiBuilder.group(...)` handlers
|
||||
- `server.ts` composes all route groups into one `HttpRouter.serve` layer with shared middleware (auth, instance lookup)
|
||||
- tests use `ExperimentalHttpApiServer.layerTest` to run against a real in-process HTTP server
|
||||
- `server.ts` composes all route groups into one `HttpRouter.toWebHandler(...)` bridge with shared middleware (auth, instance lookup)
|
||||
- tests should verify the bridged routes through the normal server surface
|
||||
|
||||
## Example migration shape
|
||||
|
||||
@@ -283,33 +319,33 @@ Each route-group spike should follow the same shape.
|
||||
- keep handler bodies thin
|
||||
- keep transport mapping at the HTTP boundary only
|
||||
|
||||
### 3. Standalone server
|
||||
### 3. Bridged server
|
||||
|
||||
- the Effect HTTP server is self-contained in `httpapi/server.ts`
|
||||
- it is **not** mounted into the Hono app — no bridge, no `toWebHandler`
|
||||
- route paths use the `/experimental/httpapi` prefix so they match the eventual cutover
|
||||
- each route group exposes its own OpenAPI doc endpoint
|
||||
- the Effect HTTP layer is composed in `httpapi/server.ts`
|
||||
- it is mounted into the Hono app via `HttpRouter.toWebHandler(...)`
|
||||
- routes keep their normal instance paths and are gated by the `KILO_EXPERIMENTAL_HTTPAPI` flag
|
||||
- the legacy Hono handlers stay registered after the bridge so current OpenAPI / SDK generation still works
|
||||
|
||||
### 4. Verification
|
||||
|
||||
- seed real state through the existing service
|
||||
- call the experimental endpoints
|
||||
- call the bridged endpoints with the flag enabled
|
||||
- assert that the service behavior is unchanged
|
||||
- assert that the generated OpenAPI contains the migrated paths and schemas
|
||||
|
||||
## Boundary composition
|
||||
|
||||
The standalone Effect server owns its own middleware stack. It does not share middleware with the Hono server.
|
||||
The Effect `HttpApi` layer owns its own auth and instance middleware, but it is currently mounted inside the existing Hono server.
|
||||
|
||||
### Auth
|
||||
|
||||
- the standalone server implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic`
|
||||
- the bridged `HttpApi` layer implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic`
|
||||
- each route group's `HttpApi` is wrapped with `.middleware(Authorization)` before being served
|
||||
- this is independent of the Hono `AuthMiddleware` — when the Effect server eventually replaces Hono, this becomes the only auth layer
|
||||
- this is independent of the Hono auth layer; the current bridge keeps the responsibility local to the `HttpApi` slice
|
||||
|
||||
### Instance and workspace lookup
|
||||
|
||||
- the standalone server resolves instance context via an `HttpRouter.middleware` that reads `x-kilo-directory` headers and `directory` query params
|
||||
- the bridged `HttpApi` layer resolves instance context via an `HttpRouter.middleware` that reads `x-kilo-directory` headers and `directory` query params
|
||||
- this is the Effect equivalent of the Hono `WorkspaceRouterMiddleware`
|
||||
- `HttpApi` handlers yield services from context and assume the correct instance has already been provided
|
||||
|
||||
@@ -324,7 +360,7 @@ The standalone Effect server owns its own middleware stack. It does not share mi
|
||||
|
||||
The first slice is successful if:
|
||||
|
||||
- the standalone Effect server starts and serves the endpoints independently of the Hono server
|
||||
- the bridged endpoints serve correctly through the existing Hono host when the flag is enabled
|
||||
- the handlers reuse the existing Effect service
|
||||
- request decoding and response shapes are schema-defined from canonical Effect schemas
|
||||
- any remaining Zod boundary usage is derived from `.zod` or clearly temporary
|
||||
@@ -365,17 +401,16 @@ Current instance route inventory:
|
||||
endpoints: `GET /question`, `POST /question/:requestID/reply`, `POST /question/:requestID/reject`
|
||||
- `permission` - `bridged`
|
||||
endpoints: `GET /permission`, `POST /permission/:requestID/reply`
|
||||
- `provider` - `bridged` (partial)
|
||||
bridged endpoint: `GET /provider/auth`
|
||||
not yet ported: `GET /provider`, OAuth mutations
|
||||
- `config` - `next`
|
||||
best next endpoint: `GET /config/providers`
|
||||
- `provider` - `bridged`
|
||||
endpoints: `GET /provider`, `GET /provider/auth`, `POST /provider/:providerID/oauth/authorize`, `POST /provider/:providerID/oauth/callback`
|
||||
- `config` - `bridged` (partial)
|
||||
bridged endpoint: `GET /config/providers`
|
||||
later endpoint: `GET /config`
|
||||
defer `PATCH /config` for now
|
||||
- `project` - `later`
|
||||
best small reads: `GET /project`, `GET /project/current`
|
||||
- `project` - `bridged` (partial)
|
||||
bridged endpoints: `GET /project`, `GET /project/current`
|
||||
defer git-init mutation first
|
||||
- `workspace` - `later`
|
||||
- `workspace` - `next`
|
||||
best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`
|
||||
defer create/remove mutations first
|
||||
- `file` - `later`
|
||||
@@ -393,12 +428,12 @@ Current instance route inventory:
|
||||
- `tui` - `defer`
|
||||
queue-style UI bridge, weak early `HttpApi` fit
|
||||
|
||||
Recommended near-term sequence after the first spike:
|
||||
Recommended near-term sequence:
|
||||
|
||||
1. `provider` auth read endpoint
|
||||
2. `config` providers read endpoint
|
||||
3. `project` read endpoints
|
||||
4. `workspace` read endpoints
|
||||
1. `workspace` read endpoints (`GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`)
|
||||
2. `config` full read endpoint (`GET /config`)
|
||||
3. `file` JSON read endpoints
|
||||
4. `mcp` JSON read endpoints
|
||||
|
||||
## Checklist
|
||||
|
||||
@@ -411,8 +446,12 @@ Recommended near-term sequence after the first spike:
|
||||
- [x] gate behind `KILO_EXPERIMENTAL_HTTPAPI` flag
|
||||
- [x] verify OTEL spans and HTTP logs flow to motel
|
||||
- [x] bridge question, permission, and provider auth routes
|
||||
- [ ] port remaining provider endpoints (`GET /provider`, OAuth mutations)
|
||||
- [ ] port `config` read endpoints
|
||||
- [x] port remaining provider endpoints (`GET /provider`, OAuth mutations)
|
||||
- [x] port `config` providers read endpoint
|
||||
- [x] port `project` read endpoints (`GET /project`, `GET /project/current`)
|
||||
- [ ] port `workspace` read endpoints
|
||||
- [ ] port `GET /config` full read endpoint
|
||||
- [ ] port `file` JSON read endpoints
|
||||
- [ ] decide when to remove the flag and make Effect routes the default
|
||||
|
||||
## Rule of thumb
|
||||
|
||||
@@ -157,7 +157,7 @@ Direct legacy usage means any source file that still calls one of:
|
||||
- `Instance.reload(...)`
|
||||
- `Instance.dispose()` / `Instance.disposeAll()`
|
||||
|
||||
Current total: `54` files in `packages/opencode/src`.
|
||||
Current total: `56` files in `packages/opencode/src`.
|
||||
|
||||
### Core bridge and plumbing
|
||||
|
||||
@@ -177,13 +177,13 @@ Migration rule:
|
||||
|
||||
These are the current request-entry seams that still create or consume instance context through the legacy helper.
|
||||
|
||||
- `src/server/instance/middleware.ts`
|
||||
- `src/server/instance/index.ts`
|
||||
- `src/server/instance/project.ts`
|
||||
- `src/server/instance/workspace.ts`
|
||||
- `src/server/instance/file.ts`
|
||||
- `src/server/instance/experimental.ts`
|
||||
- `src/server/instance/global.ts`
|
||||
- `src/server/routes/instance/middleware.ts`
|
||||
- `src/server/routes/instance/index.ts`
|
||||
- `src/server/routes/instance/project.ts`
|
||||
- `src/server/routes/control/workspace.ts`
|
||||
- `src/server/routes/instance/file.ts`
|
||||
- `src/server/routes/instance/experimental.ts`
|
||||
- `src/server/routes/global.ts`
|
||||
|
||||
Migration rule:
|
||||
|
||||
@@ -239,7 +239,7 @@ Migration rule:
|
||||
These modules are already the best near-term migration targets because they are in Effect code but still read sync getters from the legacy helper.
|
||||
|
||||
- `src/agent/agent.ts`
|
||||
- `src/config/tui-migrate.ts`
|
||||
- `src/cli/cmd/tui/config/tui-migrate.ts`
|
||||
- `src/file/index.ts`
|
||||
- `src/file/watcher.ts`
|
||||
- `src/format/formatter.ts`
|
||||
@@ -250,7 +250,7 @@ These modules are already the best near-term migration targets because they are
|
||||
- `src/project/vcs.ts`
|
||||
- `src/provider/provider.ts`
|
||||
- `src/pty/index.ts`
|
||||
- `src/session/index.ts`
|
||||
- `src/session/session.ts`
|
||||
- `src/session/instruction.ts`
|
||||
- `src/session/llm.ts`
|
||||
- `src/session/system.ts`
|
||||
|
||||
@@ -4,11 +4,11 @@ Small follow-ups that do not fit neatly into the main facade, route, tool, or sc
|
||||
|
||||
## Config / TUI
|
||||
|
||||
- [ ] `config/tui.ts` - finish the internal Effect migration after the `Instance.state(...)` removal.
|
||||
- [ ] `cli/cmd/tui/config/tui.ts` - finish the internal Effect migration.
|
||||
Keep the current precedence and migration semantics intact while converting the remaining internal async helpers (`loadState`, `mergeFile`, `loadFile`, `load`) to `Effect.gen(...)` / `Effect.fn(...)`.
|
||||
- [ ] `config/tui.ts` callers - once the internal service is stable, migrate plain async callers to use `TuiConfig.Service` directly where that actually simplifies the code.
|
||||
- [ ] `cli/cmd/tui/config/tui.ts` callers - once the internal service is stable, migrate plain async callers to use `TuiConfig.Service` directly where that actually simplifies the code.
|
||||
Likely first callers: `cli/cmd/tui/attach.ts`, `cli/cmd/tui/thread.ts`, `cli/cmd/tui/plugin/runtime.ts`.
|
||||
- [ ] `env/index.ts` - move the last production `Instance.state(...)` usage onto `InstanceState` (or its replacement) so `Instance.state` can be deleted.
|
||||
- [x] `env/index.ts` - already uses `InstanceState.make(...)`.
|
||||
|
||||
## ConfigPaths
|
||||
|
||||
@@ -21,14 +21,12 @@ Small follow-ups that do not fit neatly into the main facade, route, tool, or sc
|
||||
- `readFile(...)`
|
||||
- `parseText(...)`
|
||||
- [ ] `config/config.ts` - switch internal config loading from `Effect.promise(() => ConfigPaths.*(...))` to `yield* paths.*(...)` once the service exists.
|
||||
- [ ] `config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists.
|
||||
- [ ] `config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands.
|
||||
- [ ] `cli/cmd/tui/config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists.
|
||||
- [ ] `cli/cmd/tui/config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands.
|
||||
|
||||
## Instance cleanup
|
||||
|
||||
- [ ] `project/instance.ts` - remove `Instance.state(...)` once `env/index.ts` is migrated.
|
||||
- [ ] `project/state.ts` - delete the bespoke per-instance state helper after the last production caller is gone.
|
||||
- [ ] `test/project/state.test.ts` - replace or delete the old `Instance.state(...)` tests after the removal.
|
||||
- [ ] `project/instance.ts` - keep shrinking the legacy ALS / Promise cache after the remaining `Instance.*` callers move over.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -19,53 +19,43 @@ See `instance-context.md` for the phased plan to remove the legacy ALS / promise
|
||||
|
||||
## Service shape
|
||||
|
||||
Every service follows the same pattern — a single namespace with the service definition, layer, `runPromise`, and async facade functions:
|
||||
Every service follows the same pattern: one module, flat top-level exports, traced Effect methods, and a self-reexport at the bottom when the file is the public module.
|
||||
|
||||
```ts
|
||||
export namespace Foo {
|
||||
export interface Interface {
|
||||
readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
// For instance-scoped services:
|
||||
const state = yield* InstanceState.make<State>(
|
||||
Effect.fn("Foo.state")(() => Effect.succeed({ ... })),
|
||||
)
|
||||
|
||||
const get = Effect.fn("Foo.get")(function* (id: FooID) {
|
||||
const s = yield* InstanceState.get(state)
|
||||
// ...
|
||||
})
|
||||
|
||||
return Service.of({ get })
|
||||
}),
|
||||
)
|
||||
|
||||
// Optional: wire dependencies
|
||||
export const defaultLayer = layer.pipe(Layer.provide(FooDep.layer))
|
||||
|
||||
// Per-service runtime (inside the namespace)
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
// Async facade functions
|
||||
export async function get(id: FooID) {
|
||||
return runPromise((svc) => svc.get(id))
|
||||
}
|
||||
export interface Interface {
|
||||
readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* InstanceState.make<State>(
|
||||
Effect.fn("Foo.state")(() => Effect.succeed({ ... })),
|
||||
)
|
||||
|
||||
const get = Effect.fn("Foo.get")(function* (id: FooID) {
|
||||
const s = yield* InstanceState.get(state)
|
||||
// ...
|
||||
})
|
||||
|
||||
return Service.of({ get })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(FooDep.layer))
|
||||
|
||||
export * as Foo from "."
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Keep everything in one namespace, one file — no separate `service.ts` / `index.ts` split
|
||||
- `runPromise` goes inside the namespace (not exported unless tests need it)
|
||||
- Facade functions are plain `async function` — no `fn()` wrappers
|
||||
- Use `Effect.fn("Namespace.method")` for all Effect functions (for tracing)
|
||||
- No `Layer.fresh` — InstanceState handles per-directory isolation
|
||||
- Keep the service surface in one module; prefer flat top-level exports over `export namespace Foo { ... }`
|
||||
- Use `Effect.fn("Foo.method")` for Effect methods
|
||||
- Use a self-reexport (`export * as Foo from "."` or `"./foo"`) for the public namespace projection
|
||||
- Avoid service-local `makeRuntime(...)` facades unless a file is still intentionally in the older migration phase
|
||||
- No `Layer.fresh` for normal per-directory isolation; use `InstanceState`
|
||||
|
||||
## Schema → Zod interop
|
||||
|
||||
@@ -266,7 +256,7 @@ Tool-specific filesystem cleanup notes live in `tools.md`.
|
||||
|
||||
## Destroying the facades
|
||||
|
||||
This phase is still broadly open. As of 2026-04-13 there are still 15 `makeRuntime(...)` call sites under `src/`, with 13 still in scope for facade removal. The live checklist now lives in `facades.md`.
|
||||
This phase is no longer broadly open. There are 5 `makeRuntime(...)` call sites under `src/`, and only a small subset are still ordinary facade-removal targets. The live checklist now lives in `facades.md`.
|
||||
|
||||
These facades exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
|
||||
|
||||
@@ -297,11 +287,11 @@ For each service, the migration is roughly:
|
||||
- `ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime.
|
||||
- `SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration.
|
||||
- `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
|
||||
- `SessionRunState` — migrated 2026-04-11. Single caller in `server/instance/session.ts` converted; facade removed.
|
||||
- `Account` — migrated 2026-04-11. Callers in `server/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
|
||||
- `SessionRunState` — migrated 2026-04-11. Single caller in `server/routes/instance/session.ts` converted; facade removed.
|
||||
- `Account` — migrated 2026-04-11. Callers in `server/routes/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
|
||||
- `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed.
|
||||
- `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed.
|
||||
- `Question` — migrated 2026-04-11. Callers in `server/instance/question.ts` and test converted; facade removed.
|
||||
- `Question` — migrated 2026-04-11. Callers in `server/routes/instance/question.ts` and test converted; facade removed.
|
||||
- `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
|
||||
|
||||
## Route handler effectification
|
||||
|
||||
@@ -39,28 +39,26 @@ This eliminates multiple `runPromise` round-trips and lets handlers compose natu
|
||||
|
||||
## Current route files
|
||||
|
||||
Current instance route files live under `src/server/instance`, not `server/routes`.
|
||||
Current instance route files live under `src/server/routes/instance`.
|
||||
|
||||
The main migration targets are:
|
||||
Files that are already mostly on the intended service-yielding shape:
|
||||
|
||||
- [ ] `server/instance/session.ts` — heaviest; still has many direct facade calls for Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, Agent, Bus
|
||||
- [ ] `server/instance/global.ts` — still has direct facade calls for Config and instance lifecycle actions
|
||||
- [ ] `server/instance/provider.ts` — still has direct facade calls for Config and Provider
|
||||
- [ ] `server/instance/question.ts` — partially converted; still worth tracking here until it consistently uses the composed style
|
||||
- [ ] `server/instance/pty.ts` — still calls Pty facades directly
|
||||
- [ ] `server/instance/experimental.ts` — mixed state; some handlers are already composed, others still use facades
|
||||
- [x] `server/routes/instance/question.ts` — handlers yield `Question.Service`
|
||||
- [x] `server/routes/instance/provider.ts` — handlers yield `Provider.Service`, `ProviderAuth.Service`, and `Config.Service`
|
||||
- [x] `server/routes/instance/permission.ts` — handlers yield `Permission.Service`
|
||||
- [x] `server/routes/instance/mcp.ts` — handlers mostly yield `MCP.Service`
|
||||
- [x] `server/routes/instance/pty.ts` — handlers yield `Pty.Service`
|
||||
|
||||
Additional route files that still participate in the migration:
|
||||
Files still worth tracking here:
|
||||
|
||||
- [ ] `server/instance/index.ts` — Vcs, Agent, Skill, LSP, Format
|
||||
- [ ] `server/instance/file.ts` — Ripgrep, File, LSP
|
||||
- [ ] `server/instance/mcp.ts` — MCP facade-heavy
|
||||
- [ ] `server/instance/permission.ts` — Permission
|
||||
- [ ] `server/instance/workspace.ts` — Workspace
|
||||
- [ ] `server/instance/tui.ts` — Bus and Session
|
||||
- [ ] `server/instance/middleware.ts` — Session and Workspace lookups
|
||||
- [ ] `server/routes/instance/session.ts` — still the heaviest mixed file; many handlers are composed, but the file still mixes patterns and has direct `Bus.publish(...)` / `Session.list(...)` usage
|
||||
- [ ] `server/routes/instance/index.ts` — mostly converted, but still has direct `Instance.dispose()` / `Instance.*` reads for `/instance/dispose` and `/path`
|
||||
- [ ] `server/routes/instance/file.ts` — most handlers yield services, but `/find` still passes `Instance.directory` directly into ripgrep and `/find/symbol` is still stubbed
|
||||
- [ ] `server/routes/instance/experimental.ts` — mixed state; many handlers are composed, but some still rely on `runRequest(...)` or direct `Instance.project` reads
|
||||
- [ ] `server/routes/instance/middleware.ts` — still enters the instance via `Instance.provide(...)`
|
||||
- [ ] `server/routes/global.ts` — still uses `Instance.disposeAll()` and remains partly outside the fully-composed style
|
||||
|
||||
## Notes
|
||||
|
||||
- Some handlers already use `AppRuntime.runPromise(Effect.gen(...))` in isolated places. Keep pushing those files toward one consistent style.
|
||||
- Route conversion is closely tied to facade removal. As services lose `makeRuntime`-backed async exports, route handlers should switch to yielding the service directly.
|
||||
- Route conversion is now less about facade removal and more about removing the remaining direct `Instance.*` reads, `Instance.provide(...)` boundaries, and small Promise-style bridges inside route files.
|
||||
- `jsonRequest(...)` / `runRequest(...)` already provide a good intermediate shape for many handlers. The remaining cleanup is mostly consistency work in the heavier files.
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
# Schema migration
|
||||
|
||||
Practical reference for migrating data types in `packages/opencode` from Zod-first definitions to Effect Schema with Zod compatibility shims.
|
||||
Practical reference for migrating data types in `packages/opencode` from
|
||||
Zod-first definitions to Effect Schema with Zod compatibility shims.
|
||||
|
||||
## Goal
|
||||
|
||||
Use Effect Schema as the source of truth for domain models, IDs, inputs, outputs, and typed errors.
|
||||
Use Effect Schema as the source of truth for domain models, IDs, inputs,
|
||||
outputs, and typed errors. Keep Zod available at existing HTTP, tool, and
|
||||
compatibility boundaries by exposing a `.zod` static derived from the Effect
|
||||
schema via `@/util/effect-zod`.
|
||||
|
||||
Keep Zod available at existing HTTP, tool, and compatibility boundaries by exposing a `.zod` field derived from the Effect schema.
|
||||
The long-term driver is `specs/effect/http-api.md` — once the HTTP server
|
||||
moves to `@effect/platform`, every Schema-first DTO can flow through
|
||||
`HttpApi` / `HttpRouter` without a zod translation layer, and the entire
|
||||
`effect-zod` walker plus every `.zod` static can be deleted.
|
||||
|
||||
## Preferred shapes
|
||||
|
||||
@@ -24,17 +31,14 @@ export class Info extends Schema.Class<Info>("Foo.Info")({
|
||||
}
|
||||
```
|
||||
|
||||
If the class cannot reference itself cleanly during initialization, use the existing two-step pattern:
|
||||
If the class cannot reference itself cleanly during initialization, use the
|
||||
two-step `withStatics` pattern:
|
||||
|
||||
```ts
|
||||
const _Info = Schema.Struct({
|
||||
export const Info = Schema.Struct({
|
||||
id: FooID,
|
||||
name: Schema.String,
|
||||
})
|
||||
|
||||
export const Info = Object.assign(_Info, {
|
||||
zod: zod(_Info),
|
||||
})
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
```
|
||||
|
||||
### Errors
|
||||
@@ -49,27 +53,89 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Foo
|
||||
|
||||
### IDs and branded leaf types
|
||||
|
||||
Keep branded/schema-backed IDs as Effect schemas and expose `static readonly zod` for compatibility when callers still expect Zod.
|
||||
Keep branded/schema-backed IDs as Effect schemas and expose
|
||||
`static readonly zod` for compatibility when callers still expect Zod.
|
||||
|
||||
### Refinements
|
||||
|
||||
Reuse named refinements instead of re-spelling `z.number().int().positive()`
|
||||
in every schema. The `effect-zod` walker translates the Effect versions into
|
||||
the corresponding zod methods, so JSON Schema output (`type: integer`,
|
||||
`exclusiveMinimum`, `pattern`, `format: uuid`, …) is preserved.
|
||||
|
||||
```ts
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))
|
||||
const HexColor = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/))
|
||||
```
|
||||
|
||||
See `test/util/effect-zod.test.ts` for the full set of translated checks.
|
||||
|
||||
## Compatibility rule
|
||||
|
||||
During migration, route validators, tool parameters, and any existing Zod-based boundary should consume the derived `.zod` schema instead of maintaining a second hand-written Zod schema.
|
||||
During migration, route validators, tool parameters, and any existing
|
||||
Zod-based boundary should consume the derived `.zod` schema instead of
|
||||
maintaining a second hand-written Zod schema.
|
||||
|
||||
The default should be:
|
||||
|
||||
- Effect Schema owns the type
|
||||
- `.zod` exists only as a compatibility surface
|
||||
- new domain models should not start Zod-first unless there is a concrete boundary-specific need
|
||||
- new domain models should not start Zod-first unless there is a concrete
|
||||
boundary-specific need
|
||||
|
||||
## When Zod can stay
|
||||
|
||||
It is fine to keep a Zod-native schema temporarily when:
|
||||
|
||||
- the type is only used at an HTTP or tool boundary
|
||||
- the type is only used at an HTTP or tool boundary and is not reused elsewhere
|
||||
- the validator depends on Zod-only transforms or behavior not yet covered by `zod()`
|
||||
- the migration would force unrelated churn across a large call graph
|
||||
|
||||
When this happens, prefer leaving a short note or TODO rather than silently creating a parallel schema source of truth.
|
||||
When this happens, prefer leaving a short note or TODO rather than silently
|
||||
creating a parallel schema source of truth.
|
||||
|
||||
## Escape hatches
|
||||
|
||||
The walker in `@/util/effect-zod` exposes three explicit escape hatches for
|
||||
cases the pure-Schema path cannot express. Each one stays in the codebase
|
||||
only as long as its upstream or local dependency requires it — inline
|
||||
comments document when each can be deleted.
|
||||
|
||||
### `ZodOverride` annotation
|
||||
|
||||
Replaces the entire derivation with a hand-crafted zod schema. Used when:
|
||||
|
||||
- the target carries external `$ref` metadata (e.g.
|
||||
`config/model-id.ts` points at `https://models.dev/...`)
|
||||
- the target is a zod-only schema that cannot yet be expressed as Schema
|
||||
(e.g. `ConfigAgent.Info`, `ConfigPermission.Info`, `Log.Level`)
|
||||
|
||||
### `ZodPreprocess` annotation
|
||||
|
||||
Wraps the derived zod schema with `z.preprocess(fn, inner)`. Used by
|
||||
`config/permission.ts` to inject `__originalKeys` before parsing, because
|
||||
`Schema.StructWithRest` canonicalises output (known fields first, catchall
|
||||
after) and destroys the user's original property order — which permission
|
||||
rule precedence depends on.
|
||||
|
||||
Tracked upstream as `effect:core/wlh553`: "Schema: add preserveInputOrder
|
||||
(or pre-parse hook) for open structs." Once that lands, `ZodPreprocess` and
|
||||
the `__originalKeys` hack can both be deleted.
|
||||
|
||||
### Local `DeepMutable<T>` in `config/config.ts`
|
||||
|
||||
`Schema.Struct` produces `readonly` types. Some consumer code (notably the
|
||||
`Config` service) mutates `Info` objects directly, so a readonly-stripping
|
||||
utility is needed when casting the derived zod schema's output type.
|
||||
|
||||
`Types.DeepMutable` from effect-smol would be a drop-in, but it widens
|
||||
`unknown` to `{}` in the fallback branch — a bug that affects any schema
|
||||
using `Schema.Record(String, Schema.Unknown)`.
|
||||
|
||||
Tracked upstream as `effect:core/x228my`: "Types.DeepMutable widens unknown
|
||||
to `{}`." Once that lands, the local `DeepMutable` copy can be deleted and
|
||||
`Types.DeepMutable` used directly.
|
||||
|
||||
## Ordering
|
||||
|
||||
@@ -81,19 +147,179 @@ Migrate in this order:
|
||||
4. Service-local internal models
|
||||
5. Route and tool boundary validators that can switch to `.zod`
|
||||
|
||||
This keeps shared types canonical first and makes boundary updates mostly mechanical.
|
||||
This keeps shared types canonical first and makes boundary updates mostly
|
||||
mechanical.
|
||||
|
||||
## Checklist
|
||||
## Progress tracker
|
||||
|
||||
- [ ] Shared `schema.ts` leaf models are Effect Schema-first
|
||||
- [ ] Exported `Info` / `Input` / `Output` types use `Schema.Class` where appropriate
|
||||
- [ ] Domain errors use `Schema.TaggedErrorClass`
|
||||
- [ ] Migrated types expose `.zod` for back compatibility
|
||||
- [ ] Route and tool validators consume derived `.zod` instead of duplicate Zod definitions
|
||||
- [ ] New domain models default to Effect Schema first
|
||||
### `src/config/` ✅ complete
|
||||
|
||||
All of `packages/opencode/src/config/` has been migrated. Files that still
|
||||
import `z` do so only for local `ZodOverride` bridges or for `z.ZodType`
|
||||
type annotations — the `export const <Info|Spec>` values are all Effect
|
||||
Schema at source.
|
||||
|
||||
- [x] skills, formatter, console-state, mcp, lsp, permission (leaves), model-id, command, plugin, provider
|
||||
- [x] server, layout
|
||||
- [x] keybinds
|
||||
- [x] permission#Info
|
||||
- [x] agent
|
||||
- [x] config.ts root
|
||||
|
||||
### `src/*/schema.ts` leaf modules
|
||||
|
||||
These are the highest-priority next targets. Each is a small, self-contained
|
||||
schema module with a clear domain.
|
||||
|
||||
- [ ] `src/control-plane/schema.ts`
|
||||
- [ ] `src/permission/schema.ts`
|
||||
- [ ] `src/project/schema.ts`
|
||||
- [ ] `src/provider/schema.ts`
|
||||
- [ ] `src/pty/schema.ts`
|
||||
- [ ] `src/question/schema.ts`
|
||||
- [ ] `src/session/schema.ts`
|
||||
- [ ] `src/sync/schema.ts`
|
||||
- [ ] `src/tool/schema.ts`
|
||||
|
||||
### Session domain
|
||||
|
||||
Major cluster. Message + event types flow through the SSE API and every SDK
|
||||
output, so byte-identical SDK surface is critical.
|
||||
|
||||
- [ ] `src/session/compaction.ts`
|
||||
- [ ] `src/session/message-v2.ts`
|
||||
- [ ] `src/session/message.ts`
|
||||
- [ ] `src/session/prompt.ts`
|
||||
- [ ] `src/session/revert.ts`
|
||||
- [ ] `src/session/session.ts`
|
||||
- [ ] `src/session/status.ts`
|
||||
- [ ] `src/session/summary.ts`
|
||||
- [ ] `src/session/todo.ts`
|
||||
|
||||
### Provider domain
|
||||
|
||||
- [ ] `src/provider/auth.ts`
|
||||
- [ ] `src/provider/models.ts`
|
||||
- [ ] `src/provider/provider.ts`
|
||||
|
||||
### Tool schemas
|
||||
|
||||
Each tool declares its parameters via a zod schema. Tools are consumed by
|
||||
both the in-process runtime and the AI SDK's tool-calling layer, so the
|
||||
emitted JSON Schema must stay byte-identical.
|
||||
|
||||
- [ ] `src/tool/apply_patch.ts`
|
||||
- [ ] `src/tool/bash.ts`
|
||||
- [ ] `src/tool/codesearch.ts`
|
||||
- [ ] `src/tool/edit.ts`
|
||||
- [ ] `src/tool/glob.ts`
|
||||
- [ ] `src/tool/grep.ts`
|
||||
- [ ] `src/tool/invalid.ts`
|
||||
- [ ] `src/tool/lsp.ts`
|
||||
- [ ] `src/tool/multiedit.ts`
|
||||
- [ ] `src/tool/plan.ts`
|
||||
- [ ] `src/tool/question.ts`
|
||||
- [ ] `src/tool/read.ts`
|
||||
- [ ] `src/tool/registry.ts`
|
||||
- [ ] `src/tool/skill.ts`
|
||||
- [ ] `src/tool/task.ts`
|
||||
- [ ] `src/tool/todo.ts`
|
||||
- [ ] `src/tool/tool.ts`
|
||||
- [ ] `src/tool/webfetch.ts`
|
||||
- [ ] `src/tool/websearch.ts`
|
||||
- [ ] `src/tool/write.ts`
|
||||
|
||||
### HTTP route boundaries
|
||||
|
||||
Every file in `src/server/routes/` uses hono-openapi with zod validators for
|
||||
route inputs/outputs. Migrating these individually is the last step; most
|
||||
will switch to `.zod` derived from the Schema-migrated domain types above,
|
||||
which means touching them is largely mechanical once the domain side is
|
||||
done.
|
||||
|
||||
- [ ] `src/server/error.ts`
|
||||
- [ ] `src/server/event.ts`
|
||||
- [ ] `src/server/projectors.ts`
|
||||
- [ ] `src/server/routes/control/index.ts`
|
||||
- [ ] `src/server/routes/control/workspace.ts`
|
||||
- [ ] `src/server/routes/global.ts`
|
||||
- [ ] `src/server/routes/instance/index.ts`
|
||||
- [ ] `src/server/routes/instance/config.ts`
|
||||
- [ ] `src/server/routes/instance/event.ts`
|
||||
- [ ] `src/server/routes/instance/experimental.ts`
|
||||
- [ ] `src/server/routes/instance/file.ts`
|
||||
- [ ] `src/server/routes/instance/mcp.ts`
|
||||
- [ ] `src/server/routes/instance/permission.ts`
|
||||
- [ ] `src/server/routes/instance/project.ts`
|
||||
- [ ] `src/server/routes/instance/provider.ts`
|
||||
- [ ] `src/server/routes/instance/pty.ts`
|
||||
- [ ] `src/server/routes/instance/question.ts`
|
||||
- [ ] `src/server/routes/instance/session.ts`
|
||||
- [ ] `src/server/routes/instance/sync.ts`
|
||||
- [ ] `src/server/routes/instance/tui.ts`
|
||||
|
||||
The bigger prize for this group is the `@effect/platform` HTTP migration
|
||||
described in `specs/effect/http-api.md`. Once that lands, every one of
|
||||
these files changes shape entirely (`HttpApi.endpoint(...)` and friends),
|
||||
so the Schema-first domain types become a prerequisite rather than a
|
||||
sibling task.
|
||||
|
||||
### Everything else
|
||||
|
||||
Small / shared / control-plane / CLI. Mostly independent; can be done
|
||||
piecewise.
|
||||
|
||||
- [ ] `src/acp/agent.ts`
|
||||
- [ ] `src/agent/agent.ts`
|
||||
- [ ] `src/bus/bus-event.ts`
|
||||
- [ ] `src/bus/index.ts`
|
||||
- [ ] `src/cli/cmd/tui/config/tui-migrate.ts`
|
||||
- [ ] `src/cli/cmd/tui/config/tui-schema.ts`
|
||||
- [ ] `src/cli/cmd/tui/config/tui.ts`
|
||||
- [ ] `src/cli/cmd/tui/event.ts`
|
||||
- [ ] `src/cli/ui.ts`
|
||||
- [ ] `src/command/index.ts`
|
||||
- [ ] `src/control-plane/adaptors/worktree.ts`
|
||||
- [ ] `src/control-plane/types.ts`
|
||||
- [ ] `src/control-plane/workspace.ts`
|
||||
- [ ] `src/file/index.ts`
|
||||
- [ ] `src/file/ripgrep.ts`
|
||||
- [ ] `src/file/watcher.ts`
|
||||
- [ ] `src/format/index.ts`
|
||||
- [ ] `src/id/id.ts`
|
||||
- [ ] `src/ide/index.ts`
|
||||
- [ ] `src/installation/index.ts`
|
||||
- [ ] `src/lsp/client.ts`
|
||||
- [ ] `src/lsp/lsp.ts`
|
||||
- [ ] `src/mcp/auth.ts`
|
||||
- [ ] `src/patch/index.ts`
|
||||
- [ ] `src/plugin/github-copilot/models.ts`
|
||||
- [ ] `src/project/project.ts`
|
||||
- [ ] `src/project/vcs.ts`
|
||||
- [ ] `src/pty/index.ts`
|
||||
- [ ] `src/skill/index.ts`
|
||||
- [ ] `src/snapshot/index.ts`
|
||||
- [ ] `src/storage/db.ts`
|
||||
- [ ] `src/storage/storage.ts`
|
||||
- [ ] `src/sync/index.ts`
|
||||
- [ ] `src/util/fn.ts`
|
||||
- [ ] `src/util/log.ts`
|
||||
- [ ] `src/util/update-schema.ts`
|
||||
- [ ] `src/worktree/index.ts`
|
||||
|
||||
### Do-not-migrate
|
||||
|
||||
- `src/util/effect-zod.ts` — the walker itself. Stays zod-importing forever
|
||||
(it's what emits zod from Schema). Goes away only when the `.zod`
|
||||
compatibility layer is no longer needed anywhere.
|
||||
|
||||
## Notes
|
||||
|
||||
- Use `@/util/effect-zod` for all Schema -> Zod conversion.
|
||||
- Prefer one canonical schema definition. Avoid maintaining parallel Zod and Effect definitions for the same domain type.
|
||||
- Keep the migration incremental. Converting the domain model first is more valuable than converting every boundary in the same change.
|
||||
- Use `@/util/effect-zod` for all Schema → Zod conversion.
|
||||
- Prefer one canonical schema definition. Avoid maintaining parallel Zod and
|
||||
Effect definitions for the same domain type.
|
||||
- Keep the migration incremental. Converting the domain model first is more
|
||||
valuable than converting every boundary in the same change.
|
||||
- Every migrated file should leave the generated SDK output (`packages/sdk/
|
||||
openapi.json` and `packages/sdk/js/src/v2/gen/types.gen.ts`) byte-identical
|
||||
unless the change is deliberately user-visible.
|
||||
|
||||
@@ -40,13 +40,13 @@ Everything still lives in `packages/opencode`.
|
||||
Important current facts:
|
||||
|
||||
- there is no `packages/core` or `packages/cli` workspace yet
|
||||
- `packages/server` now exists as a minimal scaffold package, but it does not own any real route contracts, handlers, or runtime composition yet
|
||||
- there is no `packages/server` workspace yet on this branch
|
||||
- the main host server is still Hono-based in `src/server/server.ts`
|
||||
- current OpenAPI generation is Hono-based through `Server.openapi()` and `cli/cmd/generate.ts`
|
||||
- the Effect runtime and app layer are centralized in `src/effect/app-runtime.ts` and `src/effect/run-service.ts`
|
||||
- there is already one experimental Effect `HttpApi` slice at `src/server/instance/httpapi/question.ts`
|
||||
- that experimental slice is mounted under `/experimental/httpapi/question`
|
||||
- that experimental slice already has an end-to-end test at `test/server/question-httpapi.test.ts`
|
||||
- there are already bridged Effect `HttpApi` slices under `src/server/routes/instance/httpapi/*`
|
||||
- those slices are mounted into the Hono server behind `KILO_EXPERIMENTAL_HTTPAPI`
|
||||
- the bridge currently covers `question`, `permission`, `provider`, partial `config`, and partial `project` routes
|
||||
|
||||
This means the package split should start from an extraction path, not from greenfield package ownership.
|
||||
|
||||
@@ -209,17 +209,19 @@ Current host and route composition:
|
||||
|
||||
- `src/server/server.ts`
|
||||
- `src/server/control/index.ts`
|
||||
- `src/server/instance/index.ts`
|
||||
- `src/server/routes/instance/index.ts`
|
||||
- `src/server/middleware.ts`
|
||||
- `src/server/adapter.bun.ts`
|
||||
- `src/server/adapter.node.ts`
|
||||
|
||||
Current experimental `HttpApi` slice:
|
||||
Current bridged `HttpApi` slices:
|
||||
|
||||
- `src/server/instance/httpapi/question.ts`
|
||||
- `src/server/instance/httpapi/index.ts`
|
||||
- `src/server/instance/experimental.ts`
|
||||
- `test/server/question-httpapi.test.ts`
|
||||
- `src/server/routes/instance/httpapi/question.ts`
|
||||
- `src/server/routes/instance/httpapi/permission.ts`
|
||||
- `src/server/routes/instance/httpapi/provider.ts`
|
||||
- `src/server/routes/instance/httpapi/config.ts`
|
||||
- `src/server/routes/instance/httpapi/project.ts`
|
||||
- `src/server/routes/instance/httpapi/server.ts`
|
||||
|
||||
Current OpenAPI flow:
|
||||
|
||||
@@ -245,7 +247,7 @@ Keep in `packages/opencode` for now:
|
||||
|
||||
- `src/server/server.ts`
|
||||
- `src/server/control/index.ts`
|
||||
- `src/server/instance/*.ts`
|
||||
- `src/server/routes/**/*.ts`
|
||||
- `src/server/middleware.ts`
|
||||
- `src/server/adapter.*.ts`
|
||||
- `src/effect/app-runtime.ts`
|
||||
@@ -305,14 +307,13 @@ Bad early migration targets:
|
||||
|
||||
## First vertical slice
|
||||
|
||||
The first slice for the package split is the existing experimental `question` group.
|
||||
The first slice for the package split is still the existing `question` `HttpApi` group.
|
||||
|
||||
Why `question` first:
|
||||
|
||||
- it already exists as an experimental `HttpApi` slice
|
||||
- it already follows the desired contract and implementation split in one file
|
||||
- it is already mounted through the current Hono host
|
||||
- it already has an end-to-end test
|
||||
- it is JSON-only
|
||||
- it has low blast radius
|
||||
|
||||
@@ -357,7 +358,7 @@ Done means:
|
||||
|
||||
Scope:
|
||||
|
||||
- extract the pure `HttpApi` contract from `src/server/instance/httpapi/question.ts`
|
||||
- extract the pure `HttpApi` contract from `src/server/routes/instance/httpapi/question.ts`
|
||||
- place it in `packages/server/src/definition/question.ts`
|
||||
- aggregate it in `packages/server/src/definition/api.ts`
|
||||
- generate OpenAPI in `packages/server/src/openapi.ts`
|
||||
@@ -399,8 +400,9 @@ Scope:
|
||||
|
||||
- replace local experimental question route wiring in `packages/opencode`
|
||||
- keep the same mount path:
|
||||
- `/experimental/httpapi/question`
|
||||
- `/experimental/httpapi/question/doc`
|
||||
- `/question`
|
||||
- `/question/:requestID/reply`
|
||||
- `/question/:requestID/reject`
|
||||
|
||||
Rules:
|
||||
|
||||
@@ -569,7 +571,7 @@ For package-split PRs, validate the smallest useful thing.
|
||||
Typical validation for the first waves:
|
||||
|
||||
- `bun typecheck` in the touched package directory or directories
|
||||
- the relevant route test, especially `test/server/question-httpapi.test.ts`
|
||||
- the relevant server / route coverage for the migrated slice
|
||||
- merged OpenAPI coverage if the PR touches spec generation
|
||||
|
||||
Do not run tests from repo root.
|
||||
|
||||
@@ -36,7 +36,7 @@ This keeps tool tests aligned with the production service graph and makes follow
|
||||
|
||||
## Exported tools
|
||||
|
||||
These exported tool definitions already exist in `src/tool` and are on the current Effect-native `Tool.define(...)` path:
|
||||
These exported tool definitions currently use `Tool.define(...)` in `src/tool`:
|
||||
|
||||
- [x] `apply_patch.ts`
|
||||
- [x] `bash.ts`
|
||||
@@ -45,7 +45,6 @@ These exported tool definitions already exist in `src/tool` and are on the curre
|
||||
- [x] `glob.ts`
|
||||
- [x] `grep.ts`
|
||||
- [x] `invalid.ts`
|
||||
- [x] `ls.ts`
|
||||
- [x] `lsp.ts`
|
||||
- [x] `multiedit.ts`
|
||||
- [x] `plan.ts`
|
||||
@@ -60,7 +59,7 @@ These exported tool definitions already exist in `src/tool` and are on the curre
|
||||
|
||||
Notes:
|
||||
|
||||
- `batch.ts` is no longer a current tool file and should not be tracked here.
|
||||
- There is no current `ls.ts` tool file on this branch.
|
||||
- `truncate.ts` is an Effect service used by tools, not a tool definition itself.
|
||||
- `mcp-exa.ts`, `external-directory.ts`, and `schema.ts` are support modules, not standalone tool definitions.
|
||||
|
||||
@@ -73,7 +72,7 @@ Current spot cleanups worth tracking:
|
||||
- [ ] `read.ts` — still bridges to Node stream / `readline` helpers and Promise-based binary detection
|
||||
- [ ] `bash.ts` — already uses Effect child-process primitives; only keep tracking shell-specific platform bridges and parser/loading details as they come up
|
||||
- [ ] `webfetch.ts` — already uses `HttpClient`; remaining work is limited to smaller boundary helpers like HTML text extraction
|
||||
- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and `ls.ts`
|
||||
- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and file-search routes
|
||||
- [ ] `patch/index.ts` — adjacent to tool migration; still has raw fs usage behind patch application
|
||||
|
||||
Notable items that are already effectively on the target path and do not need separate migration bullets right now:
|
||||
@@ -83,7 +82,6 @@ Notable items that are already effectively on the target path and do not need se
|
||||
- `write.ts`
|
||||
- `codesearch.ts`
|
||||
- `websearch.ts`
|
||||
- `ls.ts`
|
||||
- `multiedit.ts`
|
||||
- `edit.ts`
|
||||
|
||||
|
||||
@@ -31,14 +31,26 @@ export const GenerateCommand = {
|
||||
]
|
||||
}
|
||||
}
|
||||
const raw = JSON.stringify(specs, null, 2)
|
||||
// kilocode_change start - replace upstream product name in all descriptions
|
||||
const json = JSON.stringify(specs, null, 2)
|
||||
.replaceAll("OpenCode", "Kilo")
|
||||
.replaceAll("opencode.local", "kilo.local")
|
||||
.replaceAll("opencode serve", "kilo serve")
|
||||
.replaceAll("https://opencode.ai/", "https://kilo.ai/")
|
||||
// kilocode_change end
|
||||
|
||||
// Format through prettier so output is byte-identical to committed file
|
||||
// regardless of whether ./script/format.ts runs afterward.
|
||||
const prettier = await import("prettier")
|
||||
const babel = await import("prettier/plugins/babel")
|
||||
const estree = await import("prettier/plugins/estree")
|
||||
const format = prettier.format ?? prettier.default?.format
|
||||
const json = await format(raw, {
|
||||
parser: "json",
|
||||
plugins: [babel.default ?? babel, estree.default ?? estree],
|
||||
printWidth: 120,
|
||||
})
|
||||
|
||||
// Wait for stdout to finish writing before process.exit() is called
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
process.stdout.write(json, (err) => {
|
||||
|
||||
@@ -624,7 +624,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: "Toggle theme mode",
|
||||
title: mode() === "dark" ? "Switch to light mode" : "Switch to dark mode",
|
||||
value: "theme.switch_mode",
|
||||
onSelect: (dialog) => {
|
||||
setMode(mode() === "dark" ? "light" : "dark")
|
||||
|
||||
@@ -63,6 +63,7 @@ function init() {
|
||||
useKeyboard((evt) => {
|
||||
if (suspended()) return
|
||||
if (dialog.stack.length > 0) return
|
||||
if (evt.defaultPrevented) return
|
||||
for (const option of entries()) {
|
||||
if (!isEnabled(option)) continue
|
||||
if (option.keybind && keybind.match(option.keybind, evt)) {
|
||||
|
||||
@@ -163,15 +163,10 @@ export function DialogSessionList() {
|
||||
{desc}{" "}
|
||||
<span
|
||||
style={{
|
||||
fg:
|
||||
workspaceStatus === "error"
|
||||
? theme.error
|
||||
: workspaceStatus === "disconnected"
|
||||
? theme.textMuted
|
||||
: theme.success,
|
||||
fg: workspaceStatus === "connected" ? theme.success : theme.error,
|
||||
}}
|
||||
>
|
||||
■
|
||||
●
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -139,7 +139,13 @@ export async function restoreWorkspaceSession(input: {
|
||||
total: result.data.total,
|
||||
})
|
||||
|
||||
await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]).catch((err) => {
|
||||
input.project.workspace.set(input.workspaceID)
|
||||
|
||||
try {
|
||||
await input.sync.bootstrap({ fatal: false })
|
||||
} catch (e) {}
|
||||
|
||||
await Promise.all([input.project.workspace.sync(), input.sync.session.sync(input.sessionID)]).catch((err) => {
|
||||
log.error("session restore refresh failed", {
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { useKeyboard } from "@opentui/solid"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { For } from "solid-js"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
|
||||
export function DialogWorkspaceUnavailable(props: { onRestore?: () => boolean | void | Promise<boolean | void> }) {
|
||||
const dialog = useDialog()
|
||||
const { theme } = useTheme()
|
||||
const [store, setStore] = createStore({
|
||||
active: "restore" as "cancel" | "restore",
|
||||
})
|
||||
|
||||
const options = ["cancel", "restore"] as const
|
||||
|
||||
async function confirm() {
|
||||
if (store.active === "cancel") {
|
||||
dialog.clear()
|
||||
return
|
||||
}
|
||||
const result = await props.onRestore?.()
|
||||
if (result === false) return
|
||||
}
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (evt.name === "return") {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
void confirm()
|
||||
return
|
||||
}
|
||||
if (evt.name === "left") {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
setStore("active", "cancel")
|
||||
return
|
||||
}
|
||||
if (evt.name === "right") {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
setStore("active", "restore")
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||
Workspace Unavailable
|
||||
</text>
|
||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<text fg={theme.textMuted} wrapMode="word">
|
||||
This session is attached to a workspace that is no longer available.
|
||||
</text>
|
||||
<text fg={theme.textMuted} wrapMode="word">
|
||||
Would you like to restore this session into a new workspace?
|
||||
</text>
|
||||
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1} gap={1}>
|
||||
<For each={options}>
|
||||
{(item) => (
|
||||
<box
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
backgroundColor={item === store.active ? theme.primary : undefined}
|
||||
onMouseUp={() => {
|
||||
setStore("active", item)
|
||||
void confirm()
|
||||
}}
|
||||
>
|
||||
<text fg={item === store.active ? theme.selectedListItemText : theme.textMuted}>{item}</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -52,6 +52,9 @@ export type AutocompleteRef = {
|
||||
onInput: (value: string) => void
|
||||
onKeyDown: (e: KeyEvent) => void
|
||||
onCursorChange: () => void
|
||||
// kilocode_change start - let the prompt close autocomplete without mutating draft text
|
||||
dismiss: () => void
|
||||
// kilocode_change end
|
||||
visible: false | "@" | "/"
|
||||
}
|
||||
|
||||
@@ -487,6 +490,14 @@ export function Autocomplete(props: {
|
||||
})
|
||||
}
|
||||
|
||||
// kilocode_change start - keep slash text intact when overlays hide the prompt,
|
||||
// but still allow normal autocomplete dismissal to clean it up.
|
||||
function dismiss() {
|
||||
if (!store.visible) return
|
||||
command.keybinds(true)
|
||||
setStore("visible", false)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
const text = props.input().plainText
|
||||
if (store.visible === "/" && !text.endsWith(" ") && text.startsWith("/")) {
|
||||
@@ -497,15 +508,20 @@ export function Autocomplete(props: {
|
||||
draft.input = props.input().plainText
|
||||
})
|
||||
}
|
||||
command.keybinds(true)
|
||||
setStore("visible", false)
|
||||
dismiss()
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
onMount(() => {
|
||||
props.ref({
|
||||
get visible() {
|
||||
return store.visible
|
||||
},
|
||||
// kilocode_change start
|
||||
dismiss() {
|
||||
dismiss()
|
||||
},
|
||||
// kilocode_change end
|
||||
onInput(value) {
|
||||
if (store.visible) {
|
||||
if (
|
||||
|
||||
@@ -5,10 +5,11 @@ import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { Filesystem } from "@/util"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { tint, useTheme } from "@tui/context/theme"
|
||||
import { EmptyBorder, SplitBorder } from "@tui/component/border"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { useProject } from "@tui/context/project"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { useEvent } from "@tui/context/event"
|
||||
import { MessageID, PartID } from "@/session/schema"
|
||||
@@ -38,6 +39,8 @@ import { useKV } from "../../context/kv"
|
||||
import { createFadeIn } from "../../util/signal"
|
||||
import { useTextareaKeybindings } from "../textarea-keybindings"
|
||||
import { DialogSkill } from "../dialog-skill"
|
||||
import { DialogWorkspaceCreate, restoreWorkspaceSession } from "../dialog-workspace-create"
|
||||
import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable"
|
||||
import { useArgs } from "@tui/context/args"
|
||||
|
||||
export type PromptProps = {
|
||||
@@ -92,6 +95,7 @@ export function Prompt(props: PromptProps) {
|
||||
const args = useArgs()
|
||||
const sdk = useSDK()
|
||||
const route = useRoute()
|
||||
const project = useProject()
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const toast = useToast()
|
||||
@@ -243,9 +247,11 @@ export function Prompt(props: PromptProps) {
|
||||
keybind: "input_submit",
|
||||
category: "Prompt",
|
||||
hidden: true,
|
||||
onSelect: (dialog) => {
|
||||
onSelect: async (dialog) => {
|
||||
if (!input.focused) return
|
||||
void submit()
|
||||
const handled = await submit()
|
||||
if (!handled) return
|
||||
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
@@ -462,22 +468,36 @@ export function Prompt(props: PromptProps) {
|
||||
props.ref?.(undefined)
|
||||
})
|
||||
|
||||
// kilocode_change start - close autocomplete while blocking overlays hide the prompt
|
||||
createEffect(() => {
|
||||
if (props.visible === false || props.disabled) {
|
||||
auto()?.dismiss()
|
||||
}
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
createEffect(() => {
|
||||
if (!input || input.isDestroyed) return
|
||||
if (props.visible === false || dialog.stack.length > 0) {
|
||||
input.blur()
|
||||
if (input.focused) input.blur()
|
||||
return
|
||||
}
|
||||
|
||||
// Slot/plugin updates can remount the background prompt while a dialog is open.
|
||||
// Keep focus with the dialog and let the prompt reclaim it after the dialog closes.
|
||||
input.focus()
|
||||
if (!input.focused) input.focus()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!input || input.isDestroyed) return
|
||||
const capture =
|
||||
store.mode === "normal"
|
||||
? auto()?.visible
|
||||
? (["escape", "navigate", "submit", "tab"] as const)
|
||||
: (["tab"] as const)
|
||||
: undefined
|
||||
input.traits = {
|
||||
capture: auto()?.visible ? ["escape", "navigate", "submit", "tab"] : undefined,
|
||||
capture,
|
||||
suspend: !!props.disabled || store.mode === "shell",
|
||||
status: store.mode === "shell" ? "SHELL" : undefined,
|
||||
}
|
||||
@@ -624,20 +644,48 @@ export function Prompt(props: PromptProps) {
|
||||
setStore("prompt", "input", input.plainText)
|
||||
syncExtmarksWithPromptParts()
|
||||
}
|
||||
if (props.disabled) return
|
||||
if (autocomplete?.visible) return
|
||||
if (!store.prompt.input) return
|
||||
if (props.disabled) return false
|
||||
if (autocomplete?.visible) return false
|
||||
if (!store.prompt.input) return false
|
||||
const agent = local.agent.current()
|
||||
if (!agent) return
|
||||
if (!agent) return false
|
||||
const trimmed = store.prompt.input.trim()
|
||||
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
|
||||
void exit()
|
||||
return
|
||||
return true
|
||||
}
|
||||
const selectedModel = local.model.current()
|
||||
if (!selectedModel) {
|
||||
void promptModelWarning()
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
const workspaceSession = props.sessionID ? sync.session.get(props.sessionID) : undefined
|
||||
const workspaceID = workspaceSession?.workspaceID
|
||||
const workspaceStatus = workspaceID ? (project.workspace.status(workspaceID) ?? "error") : undefined
|
||||
if (props.sessionID && workspaceID && workspaceStatus !== "connected") {
|
||||
dialog.replace(() => (
|
||||
<DialogWorkspaceUnavailable
|
||||
onRestore={() => {
|
||||
dialog.replace(() => (
|
||||
<DialogWorkspaceCreate
|
||||
onSelect={(nextWorkspaceID) =>
|
||||
restoreWorkspaceSession({
|
||||
dialog,
|
||||
sdk,
|
||||
sync,
|
||||
project,
|
||||
toast,
|
||||
workspaceID: nextWorkspaceID,
|
||||
sessionID: props.sessionID!,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))
|
||||
}}
|
||||
/>
|
||||
))
|
||||
return false
|
||||
}
|
||||
|
||||
let sessionID = props.sessionID
|
||||
@@ -652,7 +700,7 @@ export function Prompt(props: PromptProps) {
|
||||
variant: "error",
|
||||
})
|
||||
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
sessionID = res.data.id
|
||||
@@ -767,6 +815,7 @@ export function Prompt(props: PromptProps) {
|
||||
})
|
||||
}, 50)
|
||||
input.clear()
|
||||
return true
|
||||
}
|
||||
const exit = useExit()
|
||||
|
||||
@@ -873,6 +922,7 @@ export function Prompt(props: PromptProps) {
|
||||
() => !!local.agent.current() && store.mode === "normal" && showVariant(),
|
||||
animationsEnabled,
|
||||
)
|
||||
const borderHighlight = createMemo(() => tint(theme.border, highlight(), agentMetaAlpha()))
|
||||
|
||||
const placeholderText = createMemo(() => {
|
||||
if (props.showPlaceholder === false) return undefined
|
||||
@@ -934,7 +984,7 @@ export function Prompt(props: PromptProps) {
|
||||
<box ref={(r) => (anchor = r)} visible={props.visible !== false}>
|
||||
<box
|
||||
border={["left"]}
|
||||
borderColor={highlight()}
|
||||
borderColor={borderHighlight()}
|
||||
customBorderChars={{
|
||||
...SplitBorder.customBorderChars,
|
||||
bottomLeft: "╹",
|
||||
@@ -1179,6 +1229,7 @@ export function Prompt(props: PromptProps) {
|
||||
</text>
|
||||
<Show when={store.mode === "normal"}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={fadeColor(theme.textMuted, modelMetaAlpha())}>·</text>
|
||||
<text
|
||||
flexShrink={0}
|
||||
fg={fadeColor(keybind.leader ? theme.textMuted : theme.text, modelMetaAlpha())}
|
||||
@@ -1212,7 +1263,7 @@ export function Prompt(props: PromptProps) {
|
||||
height={1}
|
||||
flexShrink={0} // kilocode_change - prevent border box from shrinking in narrow terminals (#6309)
|
||||
border={["left"]}
|
||||
borderColor={highlight()}
|
||||
borderColor={borderHighlight()}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
vertical: theme.backgroundElement.a !== 0 ? "╹" : " ",
|
||||
|
||||
@@ -31,7 +31,7 @@ export const TuiInfo = z
|
||||
$schema: z.string().optional(),
|
||||
theme: z.string().optional(),
|
||||
keybinds: KeybindOverride.optional(),
|
||||
plugin: ConfigPlugin.Spec.array().optional(),
|
||||
plugin: ConfigPlugin.Spec.zod.array().optional(),
|
||||
plugin_enabled: z.record(z.string(), z.boolean()).optional(),
|
||||
})
|
||||
.extend(TuiOptions.shape)
|
||||
|
||||
@@ -11,14 +11,14 @@ import { Flag } from "@/flag/flag"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { Global } from "@/global"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Npm } from "@opencode-ai/shared/npm"
|
||||
import { CurrentWorkingDirectory } from "./cwd"
|
||||
import { ConfigPlugin } from "@/config/plugin"
|
||||
import { ConfigKeybinds } from "@/config/keybinds"
|
||||
import { InstallationLocal, InstallationVersion } from "@/installation/version"
|
||||
import { makeRuntime } from "@/cli/effect/runtime"
|
||||
import { makeRuntime } from "@/effect/runtime"
|
||||
import { Filesystem, Log } from "@/util"
|
||||
import { ConfigVariable } from "@/config/variable"
|
||||
import { Npm } from "@/npm"
|
||||
|
||||
const log = Log.create({ service: "tui.config" })
|
||||
|
||||
@@ -163,7 +163,12 @@ export const layer = Layer.effect(
|
||||
(dir) =>
|
||||
npm
|
||||
.install(dir, {
|
||||
add: ["@kilocode/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)],
|
||||
add: [
|
||||
{
|
||||
name: "@kilocode/plugin",
|
||||
version: InstallationLocal ? undefined : InstallationVersion,
|
||||
},
|
||||
],
|
||||
})
|
||||
.pipe(Effect.forkScoped),
|
||||
{
|
||||
|
||||
@@ -81,7 +81,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
},
|
||||
move(direction: 1 | -1) {
|
||||
batch(() => {
|
||||
let next = agents().findIndex((x) => x.name === agentStore.current) + direction
|
||||
const current = this.current()
|
||||
if (!current) return
|
||||
let next = agents().findIndex((x) => x.name === current.name) + direction
|
||||
if (next < 0) next = agents().length - 1
|
||||
if (next >= agents().length) next = 0
|
||||
const value = agents()[next]
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createKiloClient } from "@kilocode/sdk/v2"
|
||||
import type { GlobalEvent } from "@kilocode/sdk/v2"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { batch, onCleanup, onMount } from "solid-js"
|
||||
|
||||
export type EventSource = {
|
||||
@@ -39,6 +40,8 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
let queue: GlobalEvent[] = []
|
||||
let timer: Timer | undefined
|
||||
let last = 0
|
||||
const retryDelay = 1000
|
||||
const maxRetryDelay = 30000
|
||||
|
||||
const flush = () => {
|
||||
if (queue.length === 0) return
|
||||
@@ -73,9 +76,20 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
const ctrl = new AbortController()
|
||||
sse = ctrl
|
||||
;(async () => {
|
||||
let attempt = 0
|
||||
while (true) {
|
||||
if (abort.signal.aborted || ctrl.signal.aborted) break
|
||||
const events = await sdk.global.event({ signal: ctrl.signal })
|
||||
|
||||
const events = await sdk.global.event({
|
||||
signal: ctrl.signal,
|
||||
sseMaxRetryAttempts: 0,
|
||||
})
|
||||
|
||||
if (Flag.KILO_EXPERIMENTAL_WORKSPACES) {
|
||||
// Start syncing workspaces, it's important to do this after
|
||||
// we've started listening to events
|
||||
await sdk.sync.start().catch(() => {})
|
||||
}
|
||||
|
||||
for await (const event of events.stream) {
|
||||
if (ctrl.signal.aborted) break
|
||||
@@ -84,6 +98,12 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
|
||||
if (timer) clearTimeout(timer)
|
||||
if (queue.length > 0) flush()
|
||||
attempt += 1
|
||||
if (abort.signal.aborted || ctrl.signal.aborted) break
|
||||
|
||||
// Exponential backoff
|
||||
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), maxRetryDelay)
|
||||
await new Promise((resolve) => setTimeout(resolve, backoff))
|
||||
}
|
||||
})().catch(() => {})
|
||||
}
|
||||
@@ -92,6 +112,12 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
if (props.events) {
|
||||
const unsub = await props.events.subscribe(handleEvent)
|
||||
onCleanup(unsub)
|
||||
|
||||
if (Flag.KILO_EXPERIMENTAL_WORKSPACES) {
|
||||
// Start syncing workspaces, it's important to do this after
|
||||
// we've started listening to events
|
||||
await sdk.sync.start().catch(() => {})
|
||||
}
|
||||
} else {
|
||||
startSSE()
|
||||
}
|
||||
|
||||
@@ -630,7 +630,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
return store.status
|
||||
},
|
||||
get ready() {
|
||||
return true
|
||||
// return true // kilocode_change - upstream #23037 left this debug path enabled; keep it commented so future merges do not restore eager ready state.
|
||||
if (process.env.KILO_FAST_BOOT) return true
|
||||
return store.status !== "loading"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Layer } from "effect"
|
||||
import { TuiConfig } from "./config/tui"
|
||||
import { Npm } from "@opencode-ai/shared/npm"
|
||||
import { Npm } from "@/npm"
|
||||
import { Observability } from "@/effect/observability"
|
||||
|
||||
export const CliLayer = Observability.layer.pipe(Layer.merge(TuiConfig.layer), Layer.provide(Npm.defaultLayer))
|
||||
|
||||
@@ -5,6 +5,7 @@ import HomeNews from "@/kilocode/plugins/home-news"
|
||||
import HomeOnboarding from "@/kilocode/plugins/home-onboarding"
|
||||
import KiloHomeFooter from "@/kilocode/plugins/home-footer"
|
||||
import KiloSidebarFooter from "@/kilocode/plugins/sidebar-footer"
|
||||
import KiloSidebarPr from "@/kilocode/plugins/sidebar-pr"
|
||||
import KiloSidebarUsage from "@/kilocode/plugins/sidebar-usage"
|
||||
// kilocode_change end
|
||||
import SidebarContext from "../feature-plugins/sidebar/context"
|
||||
@@ -26,6 +27,7 @@ export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [
|
||||
HomeOnboarding, // kilocode_change
|
||||
KiloHomeFooter, // kilocode_change
|
||||
KiloSidebarFooter, // kilocode_change
|
||||
KiloSidebarPr, // kilocode_change
|
||||
KiloSidebarUsage, // kilocode_change
|
||||
HomeFooter,
|
||||
HomeTips,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TuiPluginApi, TuiSlotContext, TuiSlotMap, TuiSlotProps } from "@kilocode/plugin/tui"
|
||||
import { createSlot, createSolidSlotRegistry, type JSX, type SolidPlugin } from "@opentui/solid"
|
||||
import { children, mergeProps } from "solid-js" // kilocode_change
|
||||
import { isRecord } from "@/util/record"
|
||||
|
||||
type RuntimeSlotMap = TuiSlotMap<Record<string, object>>
|
||||
@@ -21,7 +22,20 @@ function empty<Name extends string>(_props: TuiSlotProps<Name>) {
|
||||
|
||||
let view: Slot = empty
|
||||
|
||||
export const Slot: Slot = (props) => view(props)
|
||||
// kilocode_change start - stabilize fallback children so replace-mode slots
|
||||
// don't recreate stateful defaults like the session prompt on prop changes.
|
||||
// mergeProps (instead of spread) preserves SolidJS prop reactivity so things
|
||||
// like ref, visible, disabled, on_submit keep flowing through to the slot.
|
||||
export const Slot = <Name extends string>(props: TuiSlotProps<Name>) => {
|
||||
const value = children(() => props.children)
|
||||
const merged = mergeProps(props, {
|
||||
get children() {
|
||||
return value()
|
||||
},
|
||||
})
|
||||
return view(merged as TuiSlotProps<Name>)
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
function isHostSlotPlugin(value: unknown): value is HostSlotPlugin<Record<string, object>> {
|
||||
if (!isRecord(value)) return false
|
||||
|
||||
@@ -5,11 +5,11 @@ import type { TextPart } from "@kilocode/sdk/v2"
|
||||
import { Locale } from "@/util"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { useDialog, type DialogContext } from "../../ui/dialog"
|
||||
import type { PromptInfo } from "@tui/component/prompt/history"
|
||||
import { strip } from "@tui/component/prompt/part"
|
||||
|
||||
export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
|
||||
export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID?: string) => void }) {
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const sdk = useSDK()
|
||||
@@ -19,9 +19,21 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
|
||||
dialog.setSize("large")
|
||||
})
|
||||
|
||||
const options = createMemo((): DialogSelectOption<string>[] => {
|
||||
const options = createMemo((): DialogSelectOption<string | undefined>[] => {
|
||||
const messages = sync.data.message[props.sessionID] ?? []
|
||||
const result = [] as DialogSelectOption<string>[]
|
||||
const fullSession = {
|
||||
title: "Full session",
|
||||
value: undefined,
|
||||
onSelect: async (dialog: DialogContext) => {
|
||||
const forked = await sdk.client.session.fork({ sessionID: props.sessionID })
|
||||
route.navigate({
|
||||
sessionID: forked.data!.id,
|
||||
type: "session",
|
||||
})
|
||||
dialog.clear()
|
||||
},
|
||||
} satisfies DialogSelectOption<string | undefined>
|
||||
const result = [] as DialogSelectOption<string | undefined>[]
|
||||
for (const message of messages) {
|
||||
if (message.role !== "user") continue
|
||||
const part = (sync.data.part[message.id] ?? []).find(
|
||||
@@ -57,9 +69,8 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
|
||||
},
|
||||
})
|
||||
}
|
||||
result.reverse()
|
||||
return result
|
||||
return [fullSession, ...result.reverse()]
|
||||
})
|
||||
|
||||
return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Fork from message" options={options()} />
|
||||
return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Fork session" options={options()} />
|
||||
}
|
||||
|
||||
@@ -535,7 +535,7 @@ export function Session() {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Fork from message",
|
||||
title: "Fork session",
|
||||
value: "session.fork",
|
||||
keybind: "session_fork",
|
||||
category: "Session",
|
||||
@@ -546,6 +546,7 @@ export function Session() {
|
||||
dialog.replace(() => (
|
||||
<DialogForkFromTimeline
|
||||
onMove={(messageID) => {
|
||||
if (!messageID) return
|
||||
const child = scroll.getChildren().find((child) => {
|
||||
return child.id === messageID
|
||||
})
|
||||
@@ -1277,7 +1278,8 @@ export function Session() {
|
||||
<NetworkPrompt request={network()[0]} />
|
||||
</Show>
|
||||
{/* kilocode_change end */}
|
||||
<Show when={visible()}>
|
||||
{/* kilocode_change start */}
|
||||
<Show when={!session()?.parentID}>
|
||||
<TuiPluginRuntime.Slot
|
||||
name="session_prompt"
|
||||
mode="replace"
|
||||
@@ -1299,6 +1301,7 @@ export function Session() {
|
||||
/>
|
||||
</TuiPluginRuntime.Slot>
|
||||
</Show>
|
||||
{/* kilocode_change end */}
|
||||
</box>
|
||||
</Show>
|
||||
<Toast />
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
import { useProject } from "@tui/context/project"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { useTuiConfig } from "../../context/tui-config"
|
||||
import { InstallationVersion } from "@/installation/version"
|
||||
import { InstallationChannel, InstallationVersion } from "@/installation/version"
|
||||
import { TuiPluginRuntime } from "../../plugin"
|
||||
|
||||
import { getScrollAcceleration } from "../../util/scroll"
|
||||
|
||||
export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
const project = useProject()
|
||||
const sync = useSync()
|
||||
const { theme } = useTheme()
|
||||
const tuiConfig = useTuiConfig()
|
||||
const session = createMemo(() => sync.session.get(props.sessionID))
|
||||
const workspaceStatus = () => {
|
||||
const workspaceID = session()?.workspaceID
|
||||
if (!workspaceID) return "error"
|
||||
return project.workspace.status(workspaceID) ?? "error"
|
||||
}
|
||||
const workspaceLabel = () => {
|
||||
const workspaceID = session()?.workspaceID
|
||||
if (!workspaceID) return "unknown"
|
||||
const info = project.workspace.get(workspaceID)
|
||||
if (!info) return "unknown"
|
||||
return `${info.type}: ${info.name}`
|
||||
}
|
||||
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
|
||||
|
||||
return (
|
||||
@@ -48,6 +62,15 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
<text fg={theme.text}>
|
||||
<b>{session()!.title}</b>
|
||||
</text>
|
||||
<Show when={InstallationChannel !== "latest"}>
|
||||
<text fg={theme.textMuted}>{props.sessionID}</text>
|
||||
</Show>
|
||||
<Show when={session()!.workspaceID}>
|
||||
<text fg={theme.textMuted}>
|
||||
<span style={{ fg: workspaceStatus() === "connected" ? theme.success : theme.error }}>●</span>{" "}
|
||||
{workspaceLabel()}
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={session()!.share?.url}>
|
||||
<text fg={theme.textMuted}>{session()!.share!.url}</text>
|
||||
</Show>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user