Merge remote-tracking branch 'origin/main' into feature-read-xlsx-extraction

# Conflicts:
#	packages/opencode/src/tool/read.ts
This commit is contained in:
marius-kilocode
2026-05-29 17:22:42 +02:00
30 changed files with 737 additions and 402 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Support reading text from DOCX files through the read tool.
+25 -2
View File
@@ -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
+31 -15
View File
@@ -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
View File
@@ -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": {}
+24 -12
View File
@@ -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)!
+5 -5
View File
@@ -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"
+1
View File
@@ -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
}
+8 -14
View File
@@ -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
}),
),
+3 -1
View File
@@ -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),
+9 -12
View File
@@ -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),
+33
View File
@@ -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);
}
}
-1
View File
@@ -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",
}