mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-31 01:37:28 +08:00
Merge remote-tracking branch 'origin/main' into feature-read-xlsx-extraction
# Conflicts: # packages/opencode/src/tool/read.ts
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Support reading text from DOCX files through the read tool.
|
||||
@@ -7,10 +7,33 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img width="250" alt="kilo-code-logo" src="https://github.com/user-attachments/assets/bdb0c174-b9fd-40ad-a47b-f3aab9b54e8d" />
|
||||
<a href="https://kilo.ai"><img width="250" alt="kilo-code-logo" src="https://github.com/user-attachments/assets/bdb0c174-b9fd-40ad-a47b-f3aab9b54e8d" /></a>
|
||||
</p>
|
||||
|
||||
> Kilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.
|
||||
<p align="center">
|
||||
<strong>Kilo is the all-in-one agentic engineering platform.</strong><br>
|
||||
Build, ship, and iterate faster with the most popular open source coding agent.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img width="100%" alt="Kilo Code running inside VS Code" src="https://kilo.ai/_next/image?url=%2Fscreenshots%2Fvs-code%2Fvs-code-home-page-screenshot.png&w=3840&q=75">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://kilo.ai">Website</a> ·
|
||||
<a href="https://kilo.ai/install">Install</a> ·
|
||||
<a href="https://kilo.ai/landing/vs-code">IDE</a> ·
|
||||
<a href="https://kilo.ai/cli">CLI</a> ·
|
||||
<a href="https://kilo.ai/docs">Docs</a> ·
|
||||
<a href="https://kilo.ai/leaderboard">Models</a> ·
|
||||
<a href="https://kilo.ai/gateway">Gateway</a> ·
|
||||
<a href="https://kilo.ai/pricing">Pricing</a> ·
|
||||
<a href="https://kilo.ai/pricing/kilo-pass">Kilo Pass</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
500+ models. One open source agent in <a href="https://kilo.ai/install">VS Code</a>, <a href="https://kilo.ai/features/jetbrains-native">JetBrains</a>, <a href="https://kilo.ai/cli">CLI</a>, <a href="https://kilo.ai/slack">Slack</a>, and <a href="https://kilo.ai/cloud">Cloud</a>.
|
||||
</p>
|
||||
|
||||
- ✨ Generate code from natural language
|
||||
- ✅ Checks its own work
|
||||
|
||||
@@ -387,6 +387,7 @@
|
||||
"ignore": "7.0.5",
|
||||
"immer": "11.1.4",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"mammoth": "1.12.0",
|
||||
"mime-types": "3.0.2",
|
||||
"minimatch": "10.2.5",
|
||||
"npm-package-arg": "13.0.2",
|
||||
@@ -589,6 +590,7 @@
|
||||
"patchedDependencies": {
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@effect/platform-node-shared": "4.0.0-beta.46",
|
||||
@@ -2259,6 +2261,8 @@
|
||||
|
||||
"@webcontainer/env": ["@webcontainer/env@1.1.1", "", {}, "sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng=="],
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="],
|
||||
|
||||
"@xterm/addon-clipboard": ["@xterm/addon-clipboard@0.2.0", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-Dl31BCtBhLaUEECUbEiVcCLvLBbaeGYdT7NofB8OJkGTD3MWgBsaLjXvfGAD4tQNHhm6mbKyYkR7XD8kiZsdNg=="],
|
||||
|
||||
"@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="],
|
||||
@@ -2321,7 +2325,7 @@
|
||||
|
||||
"arctic": ["arctic@2.3.4", "", { "dependencies": { "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@oslojs/jwt": "0.2.0" } }, "sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
|
||||
@@ -2413,6 +2417,8 @@
|
||||
|
||||
"bl": ["bl@6.1.6", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg=="],
|
||||
|
||||
"bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="],
|
||||
|
||||
"blueimp-md5": ["blueimp-md5@2.19.0", "", {}, "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
@@ -2743,6 +2749,8 @@
|
||||
|
||||
"dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
|
||||
|
||||
"dingbat-to-unicode": ["dingbat-to-unicode@1.0.1", "", {}, "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="],
|
||||
|
||||
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
|
||||
|
||||
"dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="],
|
||||
@@ -2765,6 +2773,8 @@
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@1.0.0-beta.19-d95b7a4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-bZZKKeoRKrMVU6zKTscjrSH0+WNb1WEi3N0Jl4wEyQ7aQpTgHzdYY6IJQ1P0M74HuSJVeX4UpkFB/S6dtqLEJg=="],
|
||||
|
||||
"duck": ["duck@0.1.12", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
@@ -3369,6 +3379,8 @@
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"lop": ["lop@0.4.2", "", { "dependencies": { "duck": "^0.1.12", "option": "~0.2.1", "underscore": "^1.13.1" } }, "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw=="],
|
||||
|
||||
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="],
|
||||
@@ -3387,6 +3399,8 @@
|
||||
|
||||
"make-fetch-happen": ["make-fetch-happen@15.0.5", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg=="],
|
||||
|
||||
"mammoth": ["mammoth@1.12.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "argparse": "~1.0.3", "base64-js": "^1.5.1", "bluebird": "~3.4.0", "dingbat-to-unicode": "^1.0.1", "jszip": "^3.7.1", "lop": "^0.4.2", "path-is-absolute": "^1.0.0", "underscore": "^1.13.1", "xmlbuilder": "^10.0.0" }, "bin": { "mammoth": "bin/mammoth" } }, "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w=="],
|
||||
|
||||
"map-age-cleaner": ["map-age-cleaner@0.1.3", "", { "dependencies": { "p-defer": "^1.0.0" } }, "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w=="],
|
||||
|
||||
"markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
|
||||
@@ -3603,6 +3617,8 @@
|
||||
|
||||
"opentui-spinner": ["opentui-spinner@0.0.6", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA=="],
|
||||
|
||||
"option": ["option@0.2.4", "", {}, "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
|
||||
@@ -3667,6 +3683,8 @@
|
||||
|
||||
"path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="],
|
||||
|
||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
@@ -4019,7 +4037,7 @@
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="],
|
||||
|
||||
@@ -4359,7 +4377,7 @@
|
||||
|
||||
"xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
|
||||
|
||||
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||
"xmlbuilder": ["xmlbuilder@10.1.1", "", {}, "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg=="],
|
||||
|
||||
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
||||
|
||||
@@ -4817,6 +4835,8 @@
|
||||
|
||||
"istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
@@ -4845,6 +4865,8 @@
|
||||
|
||||
"log-symbols/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
|
||||
|
||||
"markdown-it/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"matcher/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
@@ -4995,6 +5017,8 @@
|
||||
|
||||
"tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"tedious/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||
|
||||
"test-exclude/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
|
||||
"to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
@@ -5021,6 +5045,8 @@
|
||||
|
||||
"xml2js/sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
||||
|
||||
"xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||
|
||||
"yargs-unparser/decamelize": ["decamelize@4.0.0", "", {}, "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ=="],
|
||||
|
||||
"yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
||||
@@ -5231,6 +5257,8 @@
|
||||
|
||||
"aws-sdk/xml2js/sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
||||
|
||||
"aws-sdk/xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
||||
@@ -5341,8 +5369,6 @@
|
||||
|
||||
"friendly-words/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
@@ -5399,8 +5425,6 @@
|
||||
|
||||
"qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
||||
|
||||
"read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
|
||||
@@ -5409,8 +5433,6 @@
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"supertap/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"table/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"table/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
@@ -5559,8 +5581,6 @@
|
||||
|
||||
"friendly-words/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"gray-matter/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"mocha/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"mocha/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
@@ -5593,12 +5613,8 @@
|
||||
|
||||
"qrcode/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||
|
||||
"read-yaml-file/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"supertap/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"tar-fs/tar-stream/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"test-exclude/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
+2
-1
@@ -147,7 +147,8 @@
|
||||
"patchedDependencies": {
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
|
||||
},
|
||||
"version": "7.3.17",
|
||||
"peerDependencies": {}
|
||||
|
||||
@@ -6,22 +6,34 @@
|
||||
<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">
|
||||
<a href="https://kilo.ai"><img width="250" alt="kilo-code-logo" src="https://github.com/user-attachments/assets/bdb0c174-b9fd-40ad-a47b-f3aab9b54e8d" /></a>
|
||||
</p>
|
||||
|
||||
> Kilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.
|
||||
<p align="center">
|
||||
<strong>Kilo is the all-in-one agentic engineering platform.</strong><br>
|
||||
Build, ship, and iterate faster with the most popular open source coding agent.
|
||||
</p>
|
||||
|
||||
- ✨ Generate code from natural language
|
||||
- ✅ Checks its own work
|
||||
- 🧪 Run terminal commands
|
||||
- 🌐 Automate the browser
|
||||
- ⚡ Inline autocomplete suggestions
|
||||
- 🤖 Latest AI models
|
||||
- 🎁 API keys optional
|
||||
<p align="center">
|
||||
<img width="100%" alt="Kilo Code running inside VS Code" src="https://kilo.ai/_next/image?url=%2Fscreenshots%2Fvs-code%2Fvs-code-home-page-screenshot.png&w=3840&q=75">
|
||||
</p>
|
||||
|
||||
## Quick Links
|
||||
<p align="center">
|
||||
<a href="https://kilo.ai">Website</a> ·
|
||||
<a href="https://kilo.ai/install">Install</a> ·
|
||||
<a href="https://kilo.ai/landing/vs-code">IDE</a> ·
|
||||
<a href="https://kilo.ai/cli">CLI</a> ·
|
||||
<a href="https://kilo.ai/docs">Docs</a> ·
|
||||
<a href="https://kilo.ai/leaderboard">Models</a> ·
|
||||
<a href="https://kilo.ai/gateway">Gateway</a> ·
|
||||
<a href="https://kilo.ai/pricing">Pricing</a> ·
|
||||
<a href="https://kilo.ai/pricing/kilo-pass">Kilo Pass</a>
|
||||
</p>
|
||||
|
||||
- [VS Code Marketplace](https://kilo.ai/vscode-marketplace?utm_source=Readme) (download)
|
||||
- [Official Kilo.ai Home page](https://kilo.ai) (learn more)
|
||||
<p align="center">
|
||||
500+ models. One open source agent in <a href="https://kilo.ai/install">VS Code</a>, <a href="https://kilo.ai/features/jetbrains-native">JetBrains</a>, <a href="https://kilo.ai/cli">CLI</a>, <a href="https://kilo.ai/slack">Slack</a>, and <a href="https://kilo.ai/cloud">Cloud</a>.
|
||||
</p>
|
||||
|
||||
> 🚀 **Coming from Roo Code?** Switch to Kilo and check out our [migration guide](https://kilo.ai/articles/roo-to-kilo-migration-guide)!
|
||||
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
"agent",
|
||||
"agentic",
|
||||
"coding",
|
||||
"coding-agent",
|
||||
"coding-assistant",
|
||||
"coding agent",
|
||||
"coding assistant",
|
||||
"autocomplete",
|
||||
"code-completion",
|
||||
"pair-programming",
|
||||
"code completion",
|
||||
"pair programming",
|
||||
"chat",
|
||||
"terminal",
|
||||
"chatgpt",
|
||||
@@ -49,7 +49,7 @@
|
||||
"sonnet",
|
||||
"anthropic",
|
||||
"openai",
|
||||
"zoo-code"
|
||||
"zoo code"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onStartupFinished"
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"ignore": "7.0.5",
|
||||
"immer": "11.1.4",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"mammoth": "1.12.0",
|
||||
"mime-types": "3.0.2",
|
||||
"minimatch": "10.2.5",
|
||||
"npm-package-arg": "13.0.2",
|
||||
|
||||
@@ -196,7 +196,9 @@ export namespace KiloSessions {
|
||||
).filter((p) => p.sessionID === sessionID)
|
||||
if (permissions.length > 0) return "permission"
|
||||
|
||||
const questions = (await Question.list()).filter((q) => q.sessionID === sessionID)
|
||||
const questions = (
|
||||
await AppRuntime.runPromise(Question.Service.use((svc) => svc.list()))
|
||||
).filter((q) => q.sessionID === sessionID)
|
||||
if (questions.length > 0) return "question"
|
||||
|
||||
const status = await AppRuntime.runPromise(SessionStatus.Service.use((svc) => svc.get(SessionID.make(sessionID))))
|
||||
|
||||
@@ -77,6 +77,11 @@ export namespace RemoteSender {
|
||||
readonly list: () => Promise<ReadonlyArray<Permission.Request>>
|
||||
readonly reply: (input: Permission.ReplyInput) => Promise<boolean>
|
||||
}
|
||||
question?: {
|
||||
readonly list: () => Promise<ReadonlyArray<Question.Request>>
|
||||
readonly reply: (input: Parameters<Question.Interface["reply"]>[0]) => Promise<void>
|
||||
readonly reject: (requestID: QuestionID) => Promise<void>
|
||||
}
|
||||
prompt?: (input: SessionPrompt.PromptInput) => Promise<unknown>
|
||||
}
|
||||
|
||||
@@ -99,6 +104,20 @@ export namespace RemoteSender {
|
||||
return AppRuntime.runPromise(Permission.Service.use((svc) => svc.reply(input)))
|
||||
},
|
||||
}
|
||||
const question = options.question ?? {
|
||||
list: async () => {
|
||||
const { AppRuntime } = await import("@/effect/app-runtime")
|
||||
return AppRuntime.runPromise(Question.Service.use((svc) => svc.list()))
|
||||
},
|
||||
reply: async (input: Parameters<Question.Interface["reply"]>[0]) => {
|
||||
const { AppRuntime } = await import("@/effect/app-runtime")
|
||||
return AppRuntime.runPromise(Question.Service.use((svc) => svc.reply(input)))
|
||||
},
|
||||
reject: async (requestID: QuestionID) => {
|
||||
const { AppRuntime } = await import("@/effect/app-runtime")
|
||||
return AppRuntime.runPromise(Question.Service.use((svc) => svc.reject(requestID)))
|
||||
},
|
||||
}
|
||||
const prompt =
|
||||
options.prompt ??
|
||||
(async (input: SessionPrompt.PromptInput) => {
|
||||
@@ -157,7 +176,7 @@ export namespace RemoteSender {
|
||||
async function replay(sessionId: string) {
|
||||
const [suggestions, questions, permissions] = await Promise.all([
|
||||
Suggestion.list(),
|
||||
Question.list(),
|
||||
question.list(),
|
||||
permission.list(),
|
||||
])
|
||||
for (const suggestion of suggestions) {
|
||||
@@ -324,7 +343,7 @@ export namespace RemoteSender {
|
||||
}
|
||||
const dir = msg.sessionId ? directoryFor(msg.sessionId) : Promise.resolve(options.directory)
|
||||
dispatchQuick(msg, dir, () =>
|
||||
Question.reply({ ...parsed.data, requestID: QuestionID.make(parsed.data.requestID) }),
|
||||
question.reply({ ...parsed.data, requestID: QuestionID.make(parsed.data.requestID) }),
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -339,7 +358,7 @@ export namespace RemoteSender {
|
||||
return
|
||||
}
|
||||
const dir = msg.sessionId ? directoryFor(msg.sessionId) : Promise.resolve(options.directory)
|
||||
dispatchQuick(msg, dir, () => Question.reject(QuestionID.make(parsed.data.requestID)))
|
||||
dispatchQuick(msg, dir, () => question.reject(QuestionID.make(parsed.data.requestID)))
|
||||
return
|
||||
}
|
||||
if (msg.command === "suggestion_accept") {
|
||||
|
||||
@@ -48,6 +48,9 @@ export const PlanFollowupRuntime = {
|
||||
reject(requestID: Parameters<Question.Interface["reject"]>[0]) {
|
||||
return questions().runPromise((svc) => svc.reject(requestID))
|
||||
},
|
||||
reply(input: Parameters<Question.Interface["reply"]>[0]) {
|
||||
return questions().runPromise((svc) => svc.reply(input))
|
||||
},
|
||||
},
|
||||
todo: {
|
||||
get(sessionID: SessionID) {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import mammoth from "mammoth"
|
||||
import * as path from "path"
|
||||
import { Readable } from "stream"
|
||||
|
||||
export function accepts(filepath: string) {
|
||||
return path.extname(filepath).toLowerCase() === ".docx"
|
||||
}
|
||||
|
||||
export async function open(filepath: string) {
|
||||
const result = await mammoth.extractRawText({ path: filepath }).catch((err: unknown) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
throw new Error(`Failed to extract text from DOCX file: ${filepath}\n${message}`, { cause: err })
|
||||
})
|
||||
const warnings = result.messages.filter((item) => item.type === "warning").map((item) => item.message)
|
||||
const note = warnings.length > 0 ? `\n\n(DOCX extraction warnings: ${warnings.join("; ")})` : ""
|
||||
return Readable.from([result.value + note])
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Readable } from "stream"
|
||||
import * as Docx from "./read-docx"
|
||||
import * as Notebook from "./notebook"
|
||||
import * as Xlsx from "./xlsx"
|
||||
|
||||
export function binary(filepath: string) {
|
||||
return Docx.accepts(filepath) || Xlsx.is(filepath)
|
||||
}
|
||||
|
||||
export async function open(filepath: string): Promise<Readable | undefined> {
|
||||
if (Docx.accepts(filepath)) return Docx.open(filepath)
|
||||
if (Xlsx.is(filepath)) return Xlsx.open(filepath)
|
||||
if (Notebook.isFile(filepath)) return Notebook.open(filepath)
|
||||
return undefined
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import { zod } from "@/util/effect-zod"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { QuestionID } from "./schema"
|
||||
import { makeRuntime } from "@/effect/run-service" // kilocode_change
|
||||
import { KiloQuestion } from "@/kilocode/question" // kilocode_change
|
||||
|
||||
const log = Log.create({ service: "question" })
|
||||
@@ -134,6 +133,10 @@ export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("Que
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Question.NotFoundError", {
|
||||
requestID: QuestionID,
|
||||
}) {}
|
||||
|
||||
interface PendingEntry {
|
||||
info: Request
|
||||
deferred: Deferred.Deferred<ReadonlyArray<Answer>, RejectedError>
|
||||
@@ -152,8 +155,8 @@ export interface Interface {
|
||||
blocking?: boolean // kilocode_change
|
||||
tool?: Tool
|
||||
}) => Effect.Effect<ReadonlyArray<Answer>, RejectedError>
|
||||
readonly reply: (input: { requestID: QuestionID; answers: ReadonlyArray<Answer> }) => Effect.Effect<void>
|
||||
readonly reject: (requestID: QuestionID) => Effect.Effect<void>
|
||||
readonly reply: (input: { requestID: QuestionID; answers: ReadonlyArray<Answer> }) => Effect.Effect<void, NotFoundError>
|
||||
readonly reject: (requestID: QuestionID) => Effect.Effect<void, NotFoundError>
|
||||
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
|
||||
readonly dismissAll: (sessionID: SessionID) => Effect.Effect<void> // kilocode_change
|
||||
}
|
||||
@@ -225,7 +228,7 @@ export const layer = Layer.effect(
|
||||
const existing = pending.get(input.requestID)
|
||||
if (!existing) {
|
||||
log.warn("reply for unknown request", { requestID: input.requestID })
|
||||
return
|
||||
return yield* new NotFoundError({ requestID: input.requestID })
|
||||
}
|
||||
pending.delete(input.requestID)
|
||||
log.info("replied", { requestID: input.requestID, answers: input.answers })
|
||||
@@ -242,7 +245,7 @@ export const layer = Layer.effect(
|
||||
const existing = pending.get(requestID)
|
||||
if (!existing) {
|
||||
log.warn("reject for unknown request", { requestID })
|
||||
return
|
||||
return yield* new NotFoundError({ requestID })
|
||||
}
|
||||
pending.delete(requestID)
|
||||
log.info("rejected", { requestID })
|
||||
@@ -273,13 +276,4 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
|
||||
|
||||
// kilocode_change start - legacy promise helpers for Kilo callsites
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
export const list = () => runPromise((svc) => svc.list())
|
||||
export const ask = (input: Parameters<Interface["ask"]>[0]) => runPromise((svc) => svc.ask(input))
|
||||
export const reply = (input: Parameters<Interface["reply"]>[0]) => runPromise((svc) => svc.reply(input))
|
||||
export const reject = (requestID: QuestionID) => runPromise((svc) => svc.reject(requestID))
|
||||
export const dismissAll = (sessionID: string) => runPromise((svc) => svc.dismissAll(SessionID.make(sessionID)))
|
||||
// kilocode_change end
|
||||
|
||||
export * as Question from "."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Question } from "@/question"
|
||||
import { QuestionID } from "@/question/schema"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi" // kilocode_change - map Question missing requests to declared 404 errors
|
||||
import { InstanceHttpApi } from "../api"
|
||||
|
||||
export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question", (handlers) =>
|
||||
@@ -16,15 +16,21 @@ export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question"
|
||||
params: { requestID: QuestionID }
|
||||
payload: Question.Reply
|
||||
}) {
|
||||
yield* svc.reply({
|
||||
requestID: ctx.params.requestID,
|
||||
answers: ctx.payload.answers,
|
||||
})
|
||||
// kilocode_change start - map missing Question requests to the declared transport error
|
||||
yield* svc
|
||||
.reply({
|
||||
requestID: ctx.params.requestID,
|
||||
answers: ctx.payload.answers,
|
||||
})
|
||||
.pipe(Effect.mapError(() => new HttpApiError.NotFound({})))
|
||||
// kilocode_change end
|
||||
return true
|
||||
})
|
||||
|
||||
const reject = Effect.fn("QuestionHttpApi.reject")(function* (ctx: { params: { requestID: QuestionID } }) {
|
||||
yield* svc.reject(ctx.params.requestID)
|
||||
// kilocode_change start - map missing Question requests to the declared transport error
|
||||
yield* svc.reject(ctx.params.requestID).pipe(Effect.mapError(() => new HttpApiError.NotFound({})))
|
||||
// kilocode_change end
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Effect } from "effect" // kilocode_change - translate Question not-found failures for the legacy route
|
||||
import { Hono } from "hono"
|
||||
import { describeRoute, validator } from "hono-openapi"
|
||||
import { resolver } from "hono-openapi"
|
||||
import { QuestionID } from "@/question/schema"
|
||||
import { Question } from "@/question"
|
||||
import { NotFoundError } from "@/storage/storage" // kilocode_change - expose upstream Question not-found behavior on legacy routes
|
||||
import z from "zod"
|
||||
import { errors } from "../../error"
|
||||
import { lazy } from "@/util/lazy"
|
||||
@@ -69,10 +71,18 @@ export const QuestionRoutes = lazy(() =>
|
||||
const params = c.req.valid("param")
|
||||
const json = c.req.valid("json")
|
||||
const svc = yield* Question.Service
|
||||
yield* svc.reply({
|
||||
requestID: params.requestID,
|
||||
answers: json.answers,
|
||||
})
|
||||
// kilocode_change start - preserve documented 404 for unknown requests
|
||||
yield* svc
|
||||
.reply({
|
||||
requestID: params.requestID,
|
||||
answers: json.answers,
|
||||
})
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
() => new NotFoundError({ message: `Question request not found: ${params.requestID}` }),
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
return true
|
||||
}),
|
||||
)
|
||||
@@ -104,7 +114,15 @@ export const QuestionRoutes = lazy(() =>
|
||||
jsonRequest("QuestionRoutes.reject", c, function* () {
|
||||
const params = c.req.valid("param")
|
||||
const svc = yield* Question.Service
|
||||
yield* svc.reject(params.requestID)
|
||||
// kilocode_change start - preserve documented 404 for unknown requests
|
||||
yield* svc
|
||||
.reject(params.requestID)
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
() => new NotFoundError({ message: `Question request not found: ${params.requestID}` }),
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
return true
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -117,6 +117,7 @@ export const layer = Layer.effect(
|
||||
const commands = yield* Command.Service
|
||||
const config = yield* Config.Service
|
||||
const permission = yield* Permission.Service
|
||||
const question = yield* Question.Service // kilocode_change - dismiss superseded pending questions through the shared service
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const mcp = yield* MCP.Service
|
||||
const lsp = yield* LSP.Service
|
||||
@@ -1440,7 +1441,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
// runLoop checks hasFollowup between steps to break out once it has been
|
||||
// enqueued during the turn.
|
||||
yield* Effect.promise(() => Suggestion.dismissAll(input.sessionID))
|
||||
yield* Effect.promise(() => Question.dismissAll(input.sessionID))
|
||||
yield* question.dismissAll(input.sessionID)
|
||||
if (input.noReply === true) return message
|
||||
return yield* KiloSessionPromptQueue.enqueue(
|
||||
input.sessionID,
|
||||
@@ -2009,6 +2010,7 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
Layer.provide(SessionProcessor.defaultLayer),
|
||||
Layer.provide(Command.defaultLayer),
|
||||
Layer.provide(Permission.defaultLayer),
|
||||
Layer.provide(Question.defaultLayer), // kilocode_change - provide pending question dismissal dependency
|
||||
Layer.provide(MCP.defaultLayer),
|
||||
Layer.provide(LSP.defaultLayer),
|
||||
Layer.provide(ToolRegistry.defaultLayer),
|
||||
|
||||
@@ -15,8 +15,7 @@ import { isPdfAttachment, sniffAttachmentMime } from "@/util/media"
|
||||
// kilocode_change start
|
||||
import * as Encoding from "../kilocode/encoding"
|
||||
import * as TextStream from "../kilocode/text-stream"
|
||||
import * as Notebook from "../kilocode/tool/notebook"
|
||||
import * as Xlsx from "../kilocode/tool/xlsx"
|
||||
import * as Extract from "../kilocode/tool/read-extract"
|
||||
// kilocode_change end
|
||||
|
||||
const DEFAULT_READ_LIMIT = 2000
|
||||
@@ -302,23 +301,20 @@ export const ReadTool = Tool.define(
|
||||
}
|
||||
}
|
||||
|
||||
// kilocode_change start - extract XLSX text before generic binary rejection
|
||||
const xlsx = Xlsx.is(filepath)
|
||||
const opts = { limit: params.limit ?? DEFAULT_READ_LIMIT, offset: params.offset || 1 }
|
||||
const read = xlsx
|
||||
? () => Xlsx.open(filepath).then((stream) => readLines(stream, opts))
|
||||
: () => lines(filepath, opts)
|
||||
if (!xlsx && isBinaryFile(filepath, sample)) {
|
||||
// kilocode_change start - route extractable binary documents through lines()
|
||||
if (!Extract.binary(filepath) && isBinaryFile(filepath, sample)) {
|
||||
return yield* Effect.fail(new Error(`Cannot read binary file: ${filepath}`))
|
||||
}
|
||||
|
||||
const file = yield* Effect.promise(read)
|
||||
// kilocode_change end
|
||||
const file = yield* Effect.promise(() =>
|
||||
lines(filepath, { limit: params.limit ?? DEFAULT_READ_LIMIT, offset: params.offset || 1 }),
|
||||
)
|
||||
if (file.count < file.offset && !(file.count === 0 && file.offset === 1)) {
|
||||
return yield* Effect.fail(
|
||||
new Error(`Offset ${file.offset} is out of range for this file (${file.count} lines)`),
|
||||
)
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>\n"].join("\n")
|
||||
output += file.raw.map((line, i) => `${i + file.offset}: ${line}`).join("\n")
|
||||
@@ -365,7 +361,8 @@ export const ReadTool = Tool.define(
|
||||
// routed through TextStream.withFallback so non-UTF-8 files are decoded via
|
||||
// iconv. The body otherwise matches upstream.
|
||||
export async function lines(filepath: string, opts: { limit: number; offset: number }) {
|
||||
if (Notebook.isFile(filepath)) return readLines(await Notebook.open(filepath), opts) // kilocode_change - extract readable notebook cells before paging
|
||||
const extracted = await Extract.open(filepath) // kilocode_change - extract supported document contents before paging
|
||||
if (extracted) return readLines(extracted, opts) // kilocode_change
|
||||
return TextStream.withFallback(filepath, (stream) => readLines(stream, opts))
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ import { SessionID, MessageID, PartID } from "../../src/session/schema"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { PlanFollowup } from "../../src/kilocode/plan-followup"
|
||||
import { Question } from "../../src/question"
|
||||
import { PlanFollowup, PlanFollowupRuntime } from "../../src/kilocode/plan-followup"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
@@ -123,7 +122,7 @@ async function seed(input: {
|
||||
|
||||
async function waitQuestion(sessionID: string) {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const list = await Question.list()
|
||||
const list = await PlanFollowupRuntime.question.list()
|
||||
const question = list.find((item) => item.sessionID === sessionID)
|
||||
if (question) return question
|
||||
await Bun.sleep(10)
|
||||
@@ -155,7 +154,7 @@ describe("plan_exit detection", () => {
|
||||
expect(question).toBeDefined()
|
||||
if (!question) return
|
||||
expect(question.questions[0].header).toBe("Implement")
|
||||
await Question.reject(question.id)
|
||||
await PlanFollowupRuntime.question.reject(question.id)
|
||||
await expect(pending).resolves.toBe("break")
|
||||
}))
|
||||
|
||||
@@ -194,7 +193,7 @@ describe("plan_exit detection", () => {
|
||||
PlanFollowup.ANSWER_CONTINUE,
|
||||
])
|
||||
expect(question.questions[0].options.find((item) => item.label === PlanFollowup.ANSWER_CONTINUE)?.mode).toBe("code")
|
||||
await Question.reject(question.id)
|
||||
await PlanFollowupRuntime.question.reject(question.id)
|
||||
await expect(pending).resolves.toBe("break")
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.KILO_CLIENT
|
||||
@@ -224,7 +223,7 @@ describe("plan_exit detection", () => {
|
||||
const question = await waitQuestion(seeded.sessionID)
|
||||
expect(question).toBeDefined()
|
||||
if (!question) return
|
||||
await Question.reply({
|
||||
await PlanFollowupRuntime.question.reply({
|
||||
requestID: question.id,
|
||||
answers: [[PlanFollowup.ANSWER_CONTINUE]],
|
||||
})
|
||||
@@ -247,7 +246,7 @@ describe("plan_exit detection", () => {
|
||||
text: "Here is a partial plan, I have questions",
|
||||
})
|
||||
expect(SessionPrompt.shouldAskPlanFollowup({ messages: seeded.messages, abort: AbortSignal.any([]) })).toBe(false)
|
||||
const list = await Question.list()
|
||||
const list = await PlanFollowupRuntime.question.list()
|
||||
expect(list).toHaveLength(0)
|
||||
}))
|
||||
|
||||
@@ -326,7 +325,7 @@ describe("plan_exit detection", () => {
|
||||
expect(SessionPrompt.shouldAskPlanFollowup({ messages, abort: AbortSignal.any([]) })).toBe(false)
|
||||
|
||||
// Confirm no questions were posted
|
||||
const list = await Question.list()
|
||||
const list = await PlanFollowupRuntime.question.list()
|
||||
expect(list).toHaveLength(0)
|
||||
}))
|
||||
|
||||
@@ -440,7 +439,7 @@ describe("plan_exit detection", () => {
|
||||
const question = await waitQuestion(seeded.sessionID)
|
||||
expect(question).toBeDefined()
|
||||
if (!question) return
|
||||
await Question.reply({
|
||||
await PlanFollowupRuntime.question.reply({
|
||||
requestID: question.id,
|
||||
answers: [[PlanFollowup.ANSWER_CONTINUE]],
|
||||
})
|
||||
@@ -543,7 +542,7 @@ describe("plan_exit detection", () => {
|
||||
expect(question).toBeDefined()
|
||||
if (!question) return
|
||||
expect(question.questions[0].header).toBe("Implement")
|
||||
await Question.reply({
|
||||
await PlanFollowupRuntime.question.reply({
|
||||
requestID: question.id,
|
||||
answers: [[PlanFollowup.ANSWER_CONTINUE]],
|
||||
})
|
||||
|
||||
@@ -36,9 +36,9 @@ describe("prompt.ts Kilo-specific invariants", () => {
|
||||
// an in-flight handle.process blocked on a pending tool prompt can return.
|
||||
// Critically, the block must NOT call state.cancel or KiloSessionPromptQueue.reserve —
|
||||
// either of those would abort the running streamText mid-tokens, which was
|
||||
// the #9332 regression. Order: dismissAll(Suggestion) → dismissAll(Question) → enqueue.
|
||||
// the #9332 regression. Order: dismissAll(Suggestion), question.dismissAll, enqueue.
|
||||
const block = content.match(
|
||||
/kilocode_change start[^\n]*unblock tools[\s\S]*?Suggestion\.dismissAll[\s\S]*?Question\.dismissAll[\s\S]*?KiloSessionPromptQueue\.enqueue/,
|
||||
/kilocode_change start[^\n]*unblock tools[\s\S]*?Suggestion\.dismissAll[\s\S]*?question\.dismissAll[\s\S]*?KiloSessionPromptQueue\.enqueue/,
|
||||
)
|
||||
expect(block).not.toBeNull()
|
||||
expect(content).not.toMatch(/state\.cancel\(input\.sessionID\)/)
|
||||
|
||||
@@ -1,174 +1,119 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { KiloSessionPromptQueue } from "../../src/kilocode/session/prompt-queue"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Question } from "../../src/question"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(Question.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
|
||||
const prompt = [
|
||||
{
|
||||
header: "Continue?",
|
||||
question: "Should I continue?",
|
||||
options: [
|
||||
{ label: "Yes", description: "Go" },
|
||||
{ label: "No", description: "Stop" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const waitFor = (question: Question.Interface, count: number) =>
|
||||
Effect.gen(function* () {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const pending = yield* question.list()
|
||||
if (pending.length >= count) return pending
|
||||
yield* Effect.sleep("10 millis")
|
||||
}
|
||||
return yield* Effect.fail(new Error(`timed out waiting for ${count} pending question request(s)`))
|
||||
})
|
||||
|
||||
describe("Question.dismissAll", () => {
|
||||
test("rejects pending asks for the target session and clears them", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
it.instance(
|
||||
"rejects pending asks for the target session and clears them",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const question = yield* Question.Service
|
||||
const sesA = SessionID.make("ses_a")
|
||||
const sesB = SessionID.make("ses_b")
|
||||
const a1 = yield* question.ask({ sessionID: sesA, questions: prompt }).pipe(Effect.forkScoped)
|
||||
const a2 = yield* question.ask({ sessionID: sesA, questions: prompt }).pipe(Effect.forkScoped)
|
||||
const b1 = yield* question.ask({ sessionID: sesB, questions: prompt }).pipe(Effect.forkScoped)
|
||||
|
||||
const a1 = Question.ask({
|
||||
sessionID: sesA,
|
||||
questions: [
|
||||
{
|
||||
header: "Continue?",
|
||||
question: "Should I continue?",
|
||||
options: [
|
||||
{ label: "Yes", description: "Go" },
|
||||
{ label: "No", description: "Stop" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}).catch((err) => {
|
||||
if (err instanceof Question.RejectedError) return "rejected"
|
||||
throw err
|
||||
})
|
||||
expect(yield* waitFor(question, 3)).toHaveLength(3)
|
||||
yield* question.dismissAll(sesA)
|
||||
|
||||
const a2 = Question.ask({
|
||||
sessionID: sesA,
|
||||
questions: [
|
||||
{
|
||||
header: "Retry?",
|
||||
question: "Try again?",
|
||||
options: [
|
||||
{ label: "Retry", description: "Retry" },
|
||||
{ label: "Cancel", description: "Cancel" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}).catch((err) => {
|
||||
if (err instanceof Question.RejectedError) return "rejected"
|
||||
throw err
|
||||
})
|
||||
|
||||
const b1 = Question.ask({
|
||||
sessionID: sesB,
|
||||
questions: [
|
||||
{
|
||||
header: "Deploy?",
|
||||
question: "Deploy now?",
|
||||
options: [
|
||||
{ label: "Ship", description: "Ship" },
|
||||
{ label: "Wait", description: "Wait" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}).catch((err) => {
|
||||
if (err instanceof Question.RejectedError) return "rejected-b"
|
||||
throw err
|
||||
})
|
||||
|
||||
// Wait for all three asks to register so we can dismiss them.
|
||||
for (let i = 0; i < 50; i++) {
|
||||
if ((await Question.list()).length >= 3) break
|
||||
await Bun.sleep(10)
|
||||
for (const fiber of [a1, a2]) {
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Question.RejectedError)
|
||||
}
|
||||
expect(await Question.list()).toHaveLength(3)
|
||||
|
||||
// Track whether B's promise settles.
|
||||
let settled = false
|
||||
b1.then(() => {
|
||||
settled = true
|
||||
})
|
||||
yield* Effect.sleep("10 millis")
|
||||
|
||||
await Question.dismissAll("ses_a")
|
||||
|
||||
expect(await a1).toBe("rejected")
|
||||
expect(await a2).toBe("rejected")
|
||||
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
expect(settled).toBe(false)
|
||||
|
||||
const remaining = await Question.list()
|
||||
const remaining = yield* question.list()
|
||||
expect(remaining).toHaveLength(1)
|
||||
expect(remaining[0]?.sessionID).toBe(sesB)
|
||||
|
||||
await Question.reject(remaining[0]!.id)
|
||||
expect(await b1).toBe("rejected-b")
|
||||
},
|
||||
})
|
||||
})
|
||||
yield* question.reject(remaining[0]!.id)
|
||||
const exit = yield* Fiber.await(b1)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Question.RejectedError)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
test("is a no-op when no questions exist", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Question.dismissAll("ses_missing")
|
||||
expect(await Question.list()).toEqual([])
|
||||
},
|
||||
})
|
||||
})
|
||||
it.instance(
|
||||
"is a no-op when no questions exist",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const question = yield* Question.Service
|
||||
yield* question.dismissAll(SessionID.make("ses_missing"))
|
||||
expect(yield* question.list()).toEqual([])
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
test("ask rejects immediately when a followup is queued on the session", async () => {
|
||||
// When a newer prompt has already been enqueued on the session, a tool
|
||||
// that subsequently calls Question.ask would otherwise block the run until
|
||||
// the user manually dismisses it. Verify the pre-emptive hasFollowup check
|
||||
// rejects with RejectedError before any pending entry is registered.
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
it.instance(
|
||||
"ask rejects immediately when a followup is queued on the session",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const question = yield* Question.Service
|
||||
const sessionID = SessionID.make("ses_auto_ask")
|
||||
const started = Promise.withResolvers<void>()
|
||||
const release = Promise.withResolvers<void>()
|
||||
|
||||
// Slot 1 stays running so activeSince is pinned to its seq.
|
||||
const first = Effect.runPromise(
|
||||
KiloSessionPromptQueue.enqueue(
|
||||
sessionID,
|
||||
MessageID.make("message_ask_1"),
|
||||
Effect.gen(function* () {
|
||||
started.resolve()
|
||||
yield* Effect.promise(() => release.promise)
|
||||
return "first" as const
|
||||
}),
|
||||
Effect.succeed("first-cancelled" as const),
|
||||
),
|
||||
)
|
||||
await started.promise
|
||||
const first = yield* KiloSessionPromptQueue.enqueue(
|
||||
sessionID,
|
||||
MessageID.make("message_ask_1"),
|
||||
Effect.gen(function* () {
|
||||
started.resolve()
|
||||
yield* Effect.promise(() => release.promise)
|
||||
return "first" as const
|
||||
}),
|
||||
Effect.succeed("first-cancelled" as const),
|
||||
).pipe(Effect.forkScoped)
|
||||
yield* Effect.promise(() => started.promise)
|
||||
|
||||
// Slot 2 arrives while slot 1 is active — latest > activeSince.
|
||||
const second = Effect.runPromise(
|
||||
KiloSessionPromptQueue.enqueue(
|
||||
sessionID,
|
||||
MessageID.make("message_ask_2"),
|
||||
Effect.succeed("second" as const),
|
||||
Effect.succeed("second-cancelled" as const),
|
||||
),
|
||||
)
|
||||
await Bun.sleep(10)
|
||||
const second = yield* KiloSessionPromptQueue.enqueue(
|
||||
sessionID,
|
||||
MessageID.make("message_ask_2"),
|
||||
Effect.succeed("second" as const),
|
||||
Effect.succeed("second-cancelled" as const),
|
||||
).pipe(Effect.forkScoped)
|
||||
yield* Effect.sleep("10 millis")
|
||||
expect(KiloSessionPromptQueue.hasFollowup(sessionID)).toBe(true)
|
||||
|
||||
await expect(
|
||||
Question.ask({
|
||||
sessionID,
|
||||
questions: [
|
||||
{
|
||||
header: "Continue?",
|
||||
question: "Should I continue?",
|
||||
options: [
|
||||
{ label: "Yes", description: "Go" },
|
||||
{ label: "No", description: "Stop" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(Question.RejectedError)
|
||||
expect(await Question.list()).toEqual([])
|
||||
const exit = yield* question.ask({ sessionID, questions: prompt }).pipe(Effect.exit)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Question.RejectedError)
|
||||
expect(yield* question.list()).toEqual([])
|
||||
|
||||
release.resolve()
|
||||
expect(await first).toBe("first")
|
||||
expect(await second).toBe("second")
|
||||
},
|
||||
})
|
||||
})
|
||||
expect(yield* Fiber.join(first)).toBe("first")
|
||||
expect(yield* Fiber.join(second)).toBe("second")
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Cause, Effect, Exit, Layer } from "effect"
|
||||
import path from "path"
|
||||
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { LSP } from "../../src/lsp/lsp"
|
||||
import { Instruction } from "../../src/session/instruction"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { ReadTool } from "../../src/tool/read"
|
||||
import { Tool } from "../../src/tool/tool"
|
||||
import { Truncate } from "../../src/tool/truncate"
|
||||
import { provideInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const ctx: Tool.Context = {
|
||||
sessionID: SessionID.make("ses_test-docx"),
|
||||
messageID: MessageID.make(""),
|
||||
callID: "",
|
||||
agent: "code",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
ask: () => Effect.void,
|
||||
}
|
||||
|
||||
const expanded: Tool.Context = { ...ctx, extra: { includeDirectoryFiles: true } }
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(
|
||||
Agent.defaultLayer,
|
||||
AppFileSystem.defaultLayer,
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
Instruction.defaultLayer,
|
||||
LSP.defaultLayer,
|
||||
Truncate.defaultLayer,
|
||||
),
|
||||
)
|
||||
|
||||
const init = Effect.fn("ReadDocxTest.init")(function* () {
|
||||
const info = yield* ReadTool
|
||||
return yield* Tool.init(info)
|
||||
})
|
||||
|
||||
const run = Effect.fn("ReadDocxTest.run")(function* (
|
||||
args: Tool.InferParameters<typeof ReadTool>,
|
||||
next: Tool.Context = ctx,
|
||||
) {
|
||||
const tool = yield* init()
|
||||
return yield* tool.execute(args, next)
|
||||
})
|
||||
|
||||
const exec = Effect.fn("ReadDocxTest.exec")(function* (
|
||||
dir: string,
|
||||
args: Tool.InferParameters<typeof ReadTool>,
|
||||
next: Tool.Context = ctx,
|
||||
) {
|
||||
return yield* provideInstance(dir)(run(args, next))
|
||||
})
|
||||
|
||||
const fail = Effect.fn("ReadDocxTest.fail")(function* (dir: string, args: Tool.InferParameters<typeof ReadTool>) {
|
||||
const exit = yield* exec(dir, args).pipe(Effect.exit)
|
||||
if (Exit.isFailure(exit)) {
|
||||
const err = Cause.squash(exit.cause)
|
||||
return err instanceof Error ? err : new Error(String(err))
|
||||
}
|
||||
throw new Error("expected read to fail")
|
||||
})
|
||||
|
||||
const put = Effect.fn("ReadDocxTest.put")(function* (filepath: string, content: string | Uint8Array) {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
yield* fs.writeWithDirs(filepath, content)
|
||||
})
|
||||
|
||||
const document = async (paragraphs: string[], extra = "") => {
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter())
|
||||
await writer.add(
|
||||
"[Content_Types].xml",
|
||||
new TextReader(
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
||||
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
|
||||
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
||||
'<Default Extension="xml" ContentType="application/xml"/>' +
|
||||
'<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>' +
|
||||
"</Types>",
|
||||
),
|
||||
)
|
||||
await writer.add(
|
||||
"_rels/.rels",
|
||||
new TextReader(
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
||||
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
||||
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>' +
|
||||
"</Relationships>",
|
||||
),
|
||||
)
|
||||
await writer.add(
|
||||
"word/document.xml",
|
||||
new TextReader(
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
||||
'<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>' +
|
||||
paragraphs.map((text) => `<w:p><w:r><w:t>${text}</w:t></w:r></w:p>`).join("") +
|
||||
extra +
|
||||
"</w:body></w:document>",
|
||||
),
|
||||
)
|
||||
return writer.close()
|
||||
}
|
||||
|
||||
describe("kilocode DOCX reads", () => {
|
||||
it.live("extracts paragraph text from .docx and .DOCX files", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const bytes = yield* Effect.promise(() => document(["First paragraph", "Second paragraph"]))
|
||||
|
||||
for (const ext of ["docx", "DOCX"]) {
|
||||
const filepath = path.join(dir, `sample.${ext}`)
|
||||
yield* put(filepath, bytes)
|
||||
const result = yield* exec(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toContain("1: First paragraph")
|
||||
expect(result.output).toContain("Second paragraph")
|
||||
expect(result.attachments).toBeUndefined()
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("applies normal read pagination to extracted text", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "paged.docx")
|
||||
yield* put(filepath, yield* Effect.promise(() => document(["First paragraph", "Second paragraph"])))
|
||||
|
||||
const result = yield* exec(dir, { filePath: filepath, limit: 1 })
|
||||
|
||||
expect(result.output).toContain("1: First paragraph")
|
||||
expect(result.output).not.toContain("Second paragraph")
|
||||
expect(result.output).toContain("Use offset=2")
|
||||
expect(result.metadata.truncated).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("fails clearly for malformed DOCX files", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "invalid.docx")
|
||||
yield* put(filepath, new Uint8Array([0x50, 0x4b, 0x03, 0x04]))
|
||||
|
||||
const err = yield* fail(dir, { filePath: filepath })
|
||||
|
||||
expect(err.message).toContain("Failed to extract text from DOCX file")
|
||||
expect(err.message).toContain(filepath)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("includes extraction warnings for unsupported document elements", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "warning.docx")
|
||||
yield* put(filepath, yield* Effect.promise(() => document(["Readable text"], "<w:unsupported/>")))
|
||||
|
||||
const result = yield* exec(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toContain("Readable text")
|
||||
expect(result.output).toContain("DOCX extraction warnings")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("does not expand DOCX content in directory reads", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const folder = path.join(dir, "folder")
|
||||
yield* put(path.join(folder, "sample.docx"), yield* Effect.promise(() => document(["Hidden paragraph"])))
|
||||
|
||||
const result = yield* exec(dir, { filePath: folder }, expanded)
|
||||
|
||||
expect(result.output).toContain("sample.docx")
|
||||
expect(result.output).not.toContain("Hidden paragraph")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("preserves PDF attachments and rejects unsupported binary files", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const pdf = path.join(dir, "sample.pdf")
|
||||
const doc = path.join(dir, "sample.doc")
|
||||
yield* put(pdf, "%PDF-1.7\nfixture")
|
||||
yield* put(doc, new Uint8Array([0x00, 0x01, 0x02]))
|
||||
|
||||
const result = yield* exec(dir, { filePath: pdf })
|
||||
const err = yield* fail(dir, { filePath: doc })
|
||||
|
||||
expect(result.output).toBe("PDF read successfully")
|
||||
expect(result.attachments?.[0].mime).toBe("application/pdf")
|
||||
expect(err.message).toContain("Cannot read binary file")
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -165,6 +165,7 @@ function makeHttp() {
|
||||
Layer.provideMerge(proc),
|
||||
Layer.provideMerge(registry),
|
||||
Layer.provideMerge(trunc),
|
||||
Layer.provideMerge(question), // kilocode_change - SessionPrompt now dismisses questions via its service dependency
|
||||
Layer.provide(Instruction.defaultLayer),
|
||||
Layer.provide(SystemPrompt.defaultLayer),
|
||||
Layer.provideMerge(deps),
|
||||
|
||||
@@ -158,6 +158,7 @@ function makeHttp() {
|
||||
Layer.provideMerge(proc),
|
||||
Layer.provideMerge(registry),
|
||||
Layer.provideMerge(trunc),
|
||||
Layer.provideMerge(question), // kilocode_change - SessionPrompt now dismisses questions via its service dependency
|
||||
Layer.provide(Instruction.defaultLayer),
|
||||
Layer.provide(SystemPrompt.defaultLayer),
|
||||
Layer.provideMerge(deps),
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Effect } from "effect"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { KiloSessionPromptQueue } from "@/kilocode/session/prompt-queue"
|
||||
import { Suggestion } from "../../src/kilocode/suggestion"
|
||||
import { Question } from "../../src/question"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Session } from "../../src/session/session"
|
||||
@@ -662,62 +661,6 @@ describe("session prompt queue", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("new prompt dismisses a pending question", async () => {
|
||||
const asked = Promise.withResolvers<void>()
|
||||
const rejected = Promise.withResolvers<void>()
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () =>
|
||||
scoped(tmp.path, async (prompt) => {
|
||||
const session = await sessions.create({ title: "Question unblock regression" })
|
||||
const offAsked = Bus.subscribe(Question.Event.Asked, (event) => {
|
||||
if (event.properties.sessionID === session.id) asked.resolve()
|
||||
})
|
||||
const offRejected = Bus.subscribe(Question.Event.Rejected, (event) => {
|
||||
if (event.properties.sessionID === session.id) rejected.resolve()
|
||||
})
|
||||
|
||||
try {
|
||||
const pending = Question.ask({
|
||||
sessionID: session.id,
|
||||
questions: [
|
||||
{
|
||||
header: "Continue?",
|
||||
question: "Should I continue?",
|
||||
options: [
|
||||
{ label: "Yes", description: "Go ahead" },
|
||||
{ label: "No", description: "Stop" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}).catch((err) => {
|
||||
if (err instanceof Question.RejectedError) return "rejected"
|
||||
throw err
|
||||
})
|
||||
|
||||
await asked.promise
|
||||
await Effect.runPromise(
|
||||
prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "code",
|
||||
parts: [{ type: "text", text: "replacement prompt" }],
|
||||
noReply: true,
|
||||
}),
|
||||
)
|
||||
await rejected.promise
|
||||
|
||||
expect(await pending).toBe("rejected")
|
||||
expect(await Question.list()).toEqual([])
|
||||
} finally {
|
||||
offAsked()
|
||||
offRejected()
|
||||
}
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
test("auto-dismisses a suggestion shown after a queued prompt", async () => {
|
||||
// Reverse ordering of the "new prompt dismisses a pending suggestion" test:
|
||||
// queue the follow-up first, then open the blocker. Suggestion.show must see
|
||||
@@ -783,70 +726,4 @@ describe("session prompt queue", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("auto-dismisses a question shown after a queued prompt", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const sessionID = SessionID.make("ses_auto_question")
|
||||
const started = Promise.withResolvers<void>()
|
||||
const release = Promise.withResolvers<void>()
|
||||
|
||||
const first = Effect.runPromise(
|
||||
KiloSessionPromptQueue.enqueue(
|
||||
sessionID,
|
||||
MessageID.make("message_auto_q_1"),
|
||||
Effect.gen(function* () {
|
||||
started.resolve()
|
||||
yield* Effect.promise(() => release.promise)
|
||||
return "first" as const
|
||||
}),
|
||||
Effect.succeed("first-cancelled" as const),
|
||||
),
|
||||
)
|
||||
await started.promise
|
||||
|
||||
const second = Effect.runPromise(
|
||||
KiloSessionPromptQueue.enqueue(
|
||||
sessionID,
|
||||
MessageID.make("message_auto_q_2"),
|
||||
Effect.succeed("second" as const),
|
||||
Effect.succeed("second-cancelled" as const),
|
||||
),
|
||||
)
|
||||
await Bun.sleep(10)
|
||||
expect(KiloSessionPromptQueue.hasFollowup(sessionID)).toBe(true)
|
||||
|
||||
let asked = 0
|
||||
const offAsked = Bus.subscribe(Question.Event.Asked, (event) => {
|
||||
if (event.properties.sessionID === sessionID) asked++
|
||||
})
|
||||
try {
|
||||
await expect(
|
||||
Question.ask({
|
||||
sessionID,
|
||||
questions: [
|
||||
{
|
||||
header: "Continue?",
|
||||
question: "Should I continue?",
|
||||
options: [
|
||||
{ label: "Yes", description: "Go ahead" },
|
||||
{ label: "No", description: "Stop" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(Question.RejectedError)
|
||||
} finally {
|
||||
offAsked()
|
||||
}
|
||||
expect(asked).toBe(0)
|
||||
expect(await Question.list()).toEqual([])
|
||||
|
||||
release.resolve()
|
||||
expect(await first).toBe("first")
|
||||
expect(await second).toBe("second")
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { RemoteWS } from "../../../src/kilo-sessions/remote-ws"
|
||||
import type { RemoteProtocol } from "../../../src/kilo-sessions/remote-protocol"
|
||||
import type { SessionPrompt } from "../../../src/session/prompt"
|
||||
import { Question } from "../../../src/question"
|
||||
import { QuestionID } from "../../../src/question/schema"
|
||||
import { Permission } from "../../../src/permission"
|
||||
import { PermissionID } from "../../../src/permission/schema"
|
||||
import { ModelID, ProviderID } from "../../../src/provider/schema"
|
||||
@@ -57,6 +58,14 @@ function permissions(items: Permission.Request[] = []) {
|
||||
}
|
||||
}
|
||||
|
||||
function questions(items: Question.Request[] = []) {
|
||||
return {
|
||||
list: async () => items,
|
||||
reply: async (_input: Parameters<Question.Interface["reply"]>[0]) => {},
|
||||
reject: async (_requestID: QuestionID) => {},
|
||||
}
|
||||
}
|
||||
|
||||
function prompts(calls: SessionPrompt.PromptInput[]) {
|
||||
return async (input: SessionPrompt.PromptInput) => {
|
||||
calls.push(input)
|
||||
@@ -461,15 +470,18 @@ describe("RemoteSender", () => {
|
||||
|
||||
test("question_reply sends response after work completes", async () => {
|
||||
const { conn, sent } = fakeConn()
|
||||
let provideCalled = false
|
||||
const calls: Parameters<Question.Interface["reply"]>[0][] = []
|
||||
const sender = RemoteSender.create({
|
||||
conn,
|
||||
directory: "/tmp/test",
|
||||
log: nolog,
|
||||
subscribe: fakeBus().subscribe,
|
||||
provide: async () => {
|
||||
provideCalled = true
|
||||
return {} as any
|
||||
provide: async <R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }) => input.fn(),
|
||||
question: {
|
||||
...questions(),
|
||||
reply: async (input) => {
|
||||
calls.push(input)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -480,12 +492,12 @@ describe("RemoteSender", () => {
|
||||
data: { requestID: "r1", answers: [["yes"]] },
|
||||
})
|
||||
|
||||
// Response not sent synchronously — waits for provide to finish
|
||||
// Response not sent synchronously - waits for provide to finish.
|
||||
expect(sent).toHaveLength(0)
|
||||
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
|
||||
expect(provideCalled).toBe(true)
|
||||
expect(calls).toEqual([{ requestID: QuestionID.make("r1"), answers: [["yes"]] }])
|
||||
expect(sent).toHaveLength(1)
|
||||
expect(sent[0]).toEqual({ type: "response", id: "req_q", result: {} })
|
||||
})
|
||||
@@ -528,8 +540,12 @@ describe("RemoteSender", () => {
|
||||
directory: "/tmp/test",
|
||||
log: nolog,
|
||||
subscribe: fakeBus().subscribe,
|
||||
provide: async () => {
|
||||
throw new Error("boom")
|
||||
provide: async <R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }) => input.fn(),
|
||||
question: {
|
||||
...questions(),
|
||||
reply: async () => {
|
||||
throw new Error("boom")
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -548,6 +564,37 @@ describe("RemoteSender", () => {
|
||||
expect(sent[0].error).toContain("boom")
|
||||
})
|
||||
|
||||
test("question_reply reports unknown request errors", async () => {
|
||||
const { conn, sent } = fakeConn()
|
||||
const sender = RemoteSender.create({
|
||||
conn,
|
||||
directory: "/tmp/test",
|
||||
log: nolog,
|
||||
subscribe: fakeBus().subscribe,
|
||||
provide: async <R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }) => input.fn(),
|
||||
question: {
|
||||
...questions(),
|
||||
reply: async (input) => {
|
||||
throw new Question.NotFoundError({ requestID: input.requestID })
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
sender.handle({
|
||||
type: "command",
|
||||
id: "req_q_missing",
|
||||
command: "question_reply",
|
||||
data: { requestID: "missing", answers: [["yes"]] },
|
||||
})
|
||||
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
|
||||
expect(sent).toHaveLength(1)
|
||||
expect(sent[0].type).toBe("response")
|
||||
expect(sent[0].id).toBe("req_q_missing")
|
||||
expect(sent[0].error).toContain("Question.NotFoundError")
|
||||
})
|
||||
|
||||
test("suggestion_accept sends response after work completes", async () => {
|
||||
const { conn, sent } = fakeConn()
|
||||
const accept = spyOn(Suggestion, "accept").mockResolvedValue(true)
|
||||
@@ -595,15 +642,18 @@ describe("RemoteSender", () => {
|
||||
|
||||
test("question_reject sends response after work completes", async () => {
|
||||
const { conn, sent } = fakeConn()
|
||||
let provideCalled = false
|
||||
const calls: QuestionID[] = []
|
||||
const sender = RemoteSender.create({
|
||||
conn,
|
||||
directory: "/tmp/test",
|
||||
log: nolog,
|
||||
subscribe: fakeBus().subscribe,
|
||||
provide: async () => {
|
||||
provideCalled = true
|
||||
return {} as any
|
||||
provide: async <R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }) => input.fn(),
|
||||
question: {
|
||||
...questions(),
|
||||
reject: async (requestID) => {
|
||||
calls.push(requestID)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -616,11 +666,42 @@ describe("RemoteSender", () => {
|
||||
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
|
||||
expect(provideCalled).toBe(true)
|
||||
expect(calls).toEqual([QuestionID.make("r1")])
|
||||
expect(sent).toHaveLength(1)
|
||||
expect(sent[0]).toEqual({ type: "response", id: "req_qr", result: {} })
|
||||
})
|
||||
|
||||
test("question_reject reports unknown request errors", async () => {
|
||||
const { conn, sent } = fakeConn()
|
||||
const sender = RemoteSender.create({
|
||||
conn,
|
||||
directory: "/tmp/test",
|
||||
log: nolog,
|
||||
subscribe: fakeBus().subscribe,
|
||||
provide: async <R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }) => input.fn(),
|
||||
question: {
|
||||
...questions(),
|
||||
reject: async (requestID) => {
|
||||
throw new Question.NotFoundError({ requestID })
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
sender.handle({
|
||||
type: "command",
|
||||
id: "req_qr_missing",
|
||||
command: "question_reject",
|
||||
data: { requestID: "missing" },
|
||||
})
|
||||
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
|
||||
expect(sent).toHaveLength(1)
|
||||
expect(sent[0].type).toBe("response")
|
||||
expect(sent[0].id).toBe("req_qr_missing")
|
||||
expect(sent[0].error).toContain("Question.NotFoundError")
|
||||
})
|
||||
|
||||
test("question_reject with invalid data sends error response", () => {
|
||||
const { conn, sent } = fakeConn()
|
||||
const sender = RemoteSender.create({
|
||||
@@ -917,10 +998,6 @@ describe("RemoteSender", () => {
|
||||
const bus = fakeBus()
|
||||
|
||||
spyOn(Suggestion, "list").mockResolvedValue([])
|
||||
spyOn(Question, "list").mockResolvedValue([
|
||||
{ id: "question_1", sessionID: "ses_target", questions: [{ type: "text", text: "Continue?" }] } as any,
|
||||
{ id: "question_2", sessionID: "ses_other", questions: [{ type: "text", text: "Unrelated?" }] } as any,
|
||||
])
|
||||
|
||||
const sender = RemoteSender.create({
|
||||
conn,
|
||||
@@ -929,6 +1006,10 @@ describe("RemoteSender", () => {
|
||||
subscribe: bus.subscribe,
|
||||
provide: async (input: any) => input.fn(),
|
||||
permission: permissions(),
|
||||
question: questions([
|
||||
{ id: "question_1", sessionID: "ses_target", questions: [{ type: "text", text: "Continue?" }] } as any,
|
||||
{ id: "question_2", sessionID: "ses_other", questions: [{ type: "text", text: "Unrelated?" }] } as any,
|
||||
]),
|
||||
})
|
||||
|
||||
sender.handle({ type: "subscribe", sessionId: "ses_target" })
|
||||
@@ -949,7 +1030,6 @@ describe("RemoteSender", () => {
|
||||
const bus = fakeBus()
|
||||
|
||||
spyOn(Suggestion, "list").mockResolvedValue([])
|
||||
spyOn(Question, "list").mockResolvedValue([])
|
||||
|
||||
const sender = RemoteSender.create({
|
||||
conn,
|
||||
@@ -957,6 +1037,7 @@ describe("RemoteSender", () => {
|
||||
log: nolog,
|
||||
subscribe: bus.subscribe,
|
||||
provide: async (input: any) => input.fn(),
|
||||
question: questions(),
|
||||
permission: permissions([
|
||||
{
|
||||
id: "permission_1",
|
||||
@@ -1004,7 +1085,6 @@ describe("RemoteSender", () => {
|
||||
spyOn(Suggestion, "list").mockResolvedValue([
|
||||
{ id: "sug_1", sessionID: "ses_other", text: "Review?", actions: [] } as any,
|
||||
])
|
||||
spyOn(Question, "list").mockResolvedValue([{ id: "question_1", sessionID: "ses_other", questions: [] } as any])
|
||||
|
||||
const sender = RemoteSender.create({
|
||||
conn,
|
||||
@@ -1012,6 +1092,7 @@ describe("RemoteSender", () => {
|
||||
log: nolog,
|
||||
subscribe: bus.subscribe,
|
||||
provide: async (input: any) => input.fn(),
|
||||
question: questions([{ id: "question_1", sessionID: "ses_other", questions: [] } as any]),
|
||||
permission: permissions([
|
||||
{
|
||||
id: "permission_1",
|
||||
@@ -1049,7 +1130,6 @@ describe("RemoteSender", () => {
|
||||
actions: [{ label: "Skip", prompt: "skip" }],
|
||||
} as any,
|
||||
])
|
||||
spyOn(Question, "list").mockResolvedValue([])
|
||||
|
||||
const sender = RemoteSender.create({
|
||||
conn,
|
||||
@@ -1058,6 +1138,7 @@ describe("RemoteSender", () => {
|
||||
subscribe: bus.subscribe,
|
||||
provide: async (input: any) => input.fn(),
|
||||
permission: permissions(),
|
||||
question: questions(),
|
||||
})
|
||||
|
||||
sender.handle({ type: "subscribe", sessionId: "ses_target" })
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { afterEach, expect, test } from "bun:test"
|
||||
import { afterEach, expect } from "bun:test" // kilocode_change - blocking behavior now uses the scoped service test helper
|
||||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { Question } from "../../src/question"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { InstanceRuntime } from "../../src/project/instance-runtime"
|
||||
import { QuestionID } from "../../src/question/schema"
|
||||
import { disposeAllInstances, provideInstance, reloadTestInstance, tmpdir, tmpdirScoped } from "../fixture/fixture"
|
||||
import { disposeAllInstances, provideInstance, reloadTestInstance, tmpdirScoped } from "../fixture/fixture" // kilocode_change - blocking coverage no longer uses the Promise facade fixture
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
@@ -15,6 +15,7 @@ const it = testEffect(Layer.mergeAll(Question.defaultLayer, CrossSpawnSpawner.de
|
||||
const askEffect = Effect.fn("QuestionTest.ask")(function* (input: {
|
||||
sessionID: SessionID
|
||||
questions: ReadonlyArray<Question.Info>
|
||||
blocking?: boolean // kilocode_change
|
||||
tool?: Question.Tool
|
||||
}) {
|
||||
const question = yield* Question.Service
|
||||
@@ -110,12 +111,11 @@ it.instance(
|
||||
)
|
||||
|
||||
// kilocode_change start - review follow-up uses non-blocking question prompts
|
||||
test("ask - preserves blocking flag", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const askPromise = Question.ask({
|
||||
it.instance(
|
||||
"ask - preserves blocking flag",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const fiber = yield* askEffect({
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
blocking: false,
|
||||
questions: [
|
||||
@@ -125,16 +125,18 @@ test("ask - preserves blocking flag", async () => {
|
||||
options: [{ label: "Start", description: "Run review" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
const pending = await Question.list()
|
||||
const pending = yield* waitForPending(1)
|
||||
expect(pending[0]?.blocking).toBe(false)
|
||||
|
||||
await Question.reject(pending[0].id)
|
||||
await expect(askPromise).rejects.toBeInstanceOf(Question.RejectedError)
|
||||
},
|
||||
})
|
||||
})
|
||||
yield* rejectEffect(pending[0].id)
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Question.RejectedError)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
// reply tests
|
||||
@@ -205,15 +207,22 @@ it.instance(
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
// kilocode_change start - preserve upstream unknown-request failure behavior during facade migration
|
||||
it.instance(
|
||||
"reply - does nothing for unknown requestID",
|
||||
"reply - fails for unknown requestID",
|
||||
() =>
|
||||
replyEffect({
|
||||
requestID: QuestionID.make("que_unknown"),
|
||||
answers: [["Option 1"]],
|
||||
Effect.gen(function* () {
|
||||
const id = QuestionID.make("que_unknown")
|
||||
const exit = yield* replyEffect({ requestID: id, answers: [["Option 1"]] }).pipe(Effect.exit)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (!Exit.isFailure(exit)) return
|
||||
const err = Cause.squash(exit.cause)
|
||||
expect(err).toBeInstanceOf(Question.NotFoundError)
|
||||
if (err instanceof Question.NotFoundError) expect(err.requestID).toBe(id)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
// reject tests
|
||||
|
||||
@@ -275,9 +284,22 @@ it.instance(
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
it.instance("reject - does nothing for unknown requestID", () => rejectEffect(QuestionID.make("que_unknown")), {
|
||||
git: true,
|
||||
})
|
||||
// kilocode_change start - preserve upstream unknown-request failure behavior during facade migration
|
||||
it.instance(
|
||||
"reject - fails for unknown requestID",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const id = QuestionID.make("que_unknown")
|
||||
const exit = yield* rejectEffect(id).pipe(Effect.exit)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (!Exit.isFailure(exit)) return
|
||||
const err = Cause.squash(exit.cause)
|
||||
expect(err).toBeInstanceOf(Question.NotFoundError)
|
||||
if (err instanceof Question.NotFoundError) expect(err.requestID).toBe(id)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
// multiple questions tests
|
||||
|
||||
|
||||
@@ -213,6 +213,7 @@ function makeHttp() {
|
||||
Layer.provideMerge(proc),
|
||||
Layer.provideMerge(registry),
|
||||
Layer.provideMerge(trunc),
|
||||
Layer.provideMerge(question), // kilocode_change - SessionPrompt now dismisses questions via its service dependency
|
||||
Layer.provide(Instruction.defaultLayer),
|
||||
Layer.provide(SystemPrompt.defaultLayer),
|
||||
Layer.provideMerge(deps),
|
||||
@@ -396,6 +397,51 @@ it.live("loop calls LLM and returns assistant message", () =>
|
||||
),
|
||||
)
|
||||
|
||||
// kilocode_change start - replacement prompts unblock pending Question service requests
|
||||
it.live("new prompt dismisses a pending question", () =>
|
||||
provideTmpdirServer(
|
||||
Effect.fnUntraced(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const question = yield* Question.Service
|
||||
const chat = yield* sessions.create({ title: "Question unblock regression" })
|
||||
const pending = yield* question
|
||||
.ask({
|
||||
sessionID: chat.id,
|
||||
questions: [
|
||||
{
|
||||
header: "Continue?",
|
||||
question: "Should I continue?",
|
||||
options: [
|
||||
{ label: "Yes", description: "Go ahead" },
|
||||
{ label: "No", description: "Stop" },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
.pipe(Effect.forkScoped)
|
||||
yield* waitFor(
|
||||
"pending question",
|
||||
question.list().pipe(Effect.map((items) => items.find((item) => item.sessionID === chat.id))),
|
||||
)
|
||||
|
||||
yield* prompt.prompt({
|
||||
sessionID: chat.id,
|
||||
agent: "build",
|
||||
parts: [{ type: "text", text: "replacement prompt" }],
|
||||
noReply: true,
|
||||
})
|
||||
|
||||
const exit = yield* Fiber.await(pending)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Question.RejectedError)
|
||||
expect(yield* question.list()).toEqual([])
|
||||
}),
|
||||
{ git: true, config: providerCfg },
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
it.live("prompt emits v2 prompted and synthetic events", () =>
|
||||
provideTmpdirServer(
|
||||
Effect.fnUntraced(function* () {
|
||||
|
||||
@@ -150,6 +150,7 @@ function makeHttp() {
|
||||
Layer.provideMerge(proc),
|
||||
Layer.provideMerge(registry),
|
||||
Layer.provideMerge(trunc),
|
||||
Layer.provideMerge(question), // kilocode_change - SessionPrompt now dismisses questions via its service dependency
|
||||
Layer.provide(Instruction.defaultLayer),
|
||||
Layer.provide(SystemPrompt.defaultLayer),
|
||||
Layer.provideMerge(deps),
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
diff --git a/lib/xml/xmldom.js b/lib/xml/xmldom.js
|
||||
index 752c353f0c97f86280c96984fe963c8b6f6d0c6c..b75bd10746eb035276cfded6d624e2cbda3c3c6e 100644
|
||||
--- a/lib/xml/xmldom.js
|
||||
+++ b/lib/xml/xmldom.js
|
||||
@@ -2,20 +2,22 @@ var xmldom = require("@xmldom/xmldom");
|
||||
var dom = require("@xmldom/xmldom/lib/dom");
|
||||
|
||||
function parseFromString(string) {
|
||||
- var error = null;
|
||||
+ var fatalError = null;
|
||||
|
||||
var domParser = new xmldom.DOMParser({
|
||||
- errorHandler: function(level, message) {
|
||||
- error = {level: level, message: message};
|
||||
+ onError: function(level, message) {
|
||||
+ if (level === "fatalError") {
|
||||
+ fatalError = message;
|
||||
+ }
|
||||
}
|
||||
});
|
||||
|
||||
- var document = domParser.parseFromString(string);
|
||||
+ var document = domParser.parseFromString(string, "text/xml");
|
||||
|
||||
- if (error === null) {
|
||||
+ if (fatalError === null) {
|
||||
return document;
|
||||
} else {
|
||||
- throw new Error(error.level + ": " + error.message);
|
||||
+ throw new Error("fatalError: " + fatalError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ const allow: Record<string, string> = {
|
||||
"bus/index.ts": "core bus callback and synchronous runtime boundary",
|
||||
"cli/cmd/tui/config/tui.ts": "separately tracked TUI config facade",
|
||||
"installation/index.ts": "existing installation facade outside #10655",
|
||||
"question/index.ts": "transitional facade deferred for upstream reconciliation in #10655",
|
||||
"session/compaction.ts": "existing compaction facade outside #10655",
|
||||
"sync/index.ts": "sync event runtime boundary",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user