mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9957b54882 | ||
|
|
ec47d3446f | ||
|
|
546040ffe7 | ||
|
|
a52ab4ab64 | ||
|
|
76b4e96508 | ||
|
|
32abd34bc0 | ||
|
|
67af018d02 |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.31.1"
|
||||
".": "0.31.4"
|
||||
}
|
||||
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## [0.31.4](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.31.3...v0.31.4) (2026-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* gate auto mode on model support ([#604](https://github.com/agentclientprotocol/claude-agent-acp/issues/604)) ([ec47d34](https://github.com/agentclientprotocol/claude-agent-acp/commit/ec47d3446fa22e5895621f57c0a4497dbe044505))
|
||||
|
||||
## [0.31.3](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.31.2...v0.31.3) (2026-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Rename effort config category to thought_level ([#609](https://github.com/agentclientprotocol/claude-agent-acp/issues/609)) ([76b4e96](https://github.com/agentclientprotocol/claude-agent-acp/commit/76b4e9650832e59a6b4915ad898fc66a065022fa))
|
||||
|
||||
## [0.31.2](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.31.1...v0.31.2) (2026-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* tolerate invalid values in settings.json ([#601](https://github.com/agentclientprotocol/claude-agent-acp/issues/601)) ([67af018](https://github.com/agentclientprotocol/claude-agent-acp/commit/67af018d0216238d8f3cd5bad04a55200518239b))
|
||||
|
||||
## [0.31.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.31.0...v0.31.1) (2026-04-27)
|
||||
|
||||
|
||||
|
||||
262
package-lock.json
generated
262
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.31.1",
|
||||
"version": "0.31.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.31.1",
|
||||
"version": "0.31.4",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.20.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.119",
|
||||
"@agentclientprotocol/sdk": "0.21.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.121",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -20,8 +20,8 @@
|
||||
"@anthropic-ai/sdk": "0.89.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@types/node": "25.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.1",
|
||||
"@typescript-eslint/parser": "8.59.1",
|
||||
"eslint": "10.2.1",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.5.0",
|
||||
@@ -32,18 +32,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@agentclientprotocol/sdk": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.20.0.tgz",
|
||||
"integrity": "sha512-BxEHyE4MvwyOsdyVPub1vEtyrq8E0JSdjC+ckXWimY1VabFCTXdPyXv2y2Omz1j+iod7Z8oBJDXFCJptM0GBqQ==",
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.21.0.tgz",
|
||||
"integrity": "sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.119.tgz",
|
||||
"integrity": "sha512-6AvthpsaOTlkn514brSGOcCSLHDXODnU+ExN1O3CJCjxr5RBcmzR057C9EIM0G7IchnXsRfMZgRO1QKsjTXdbA==",
|
||||
"version": "0.2.121",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.121.tgz",
|
||||
"integrity": "sha512-hwZNYTkGLKVixd/V/OCJwfH/SdfxZXGV0m6wvy5EBq6qfB+lvJTRz/MSOSa7dHqo4/F7zJY68crEEca68Wrxpw==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.81.0",
|
||||
@@ -53,23 +53,23 @@
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.119"
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.121",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.121",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.121",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.121",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.121",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.121",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.121",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.121"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.2.119.tgz",
|
||||
"integrity": "sha512-kxnG37SZqUata2Jcp/YQ0n9Y7o/sinE/8LdG4ltM1gePh+z+0Mfa4vBUUTEBMBFth9PTovKoesIuVuyFpvO/Cw==",
|
||||
"version": "0.2.121",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.2.121.tgz",
|
||||
"integrity": "sha512-zVHcXvx6Hl/glDcOCH+EyNx4KPE9cMGLk42eEBSZe014tAN5W8bwM/By08iM6dxijnpH0NQRNNEAW+BryWzuDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -80,9 +80,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.2.119.tgz",
|
||||
"integrity": "sha512-9Aj8g3ELsmZuOFg17TCkikeg/Wt2ucVT8hOOPQUatzLd7BKhydrHLA0RP42nBpWECO1B/n/mPdQ4iS/LS3s2Fg==",
|
||||
"version": "0.2.121",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.2.121.tgz",
|
||||
"integrity": "sha512-lIXdqKj+bpfDxCk/eU1F1TXNqsIsLTRrkUG/wx19WIGZ8gLUmmVSveUKGlNegTs7S6evMvuezprJzDJT4TcvPA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -93,15 +93,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.2.119.tgz",
|
||||
"integrity": "sha512-v3o464XkiYehp/OKidQQirxdVb+aGSvdJvHF2zH9p33W8M/NC21zwwh4dhwDnKsyrtBIgkt2CcMwzIl30r0OtA==",
|
||||
"version": "0.2.121",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.2.121.tgz",
|
||||
"integrity": "sha512-AQSnJzaiFvQpUPfO1tWLvsHgb6KNar4QYEQ/5/sk1itfgr3Fx9gxTreq43wX7AXSvkBX1QlDaP1aR1sfM/g/lQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -109,15 +106,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.2.119.tgz",
|
||||
"integrity": "sha512-IPGWgtz+gGnD7fxKAvSf913EUT/lYBTBE8EZ7lh3+x5ZP2859LWLmrCm053Lf3nMWo/CWikZsVPwkDVwpz6tIQ==",
|
||||
"version": "0.2.121",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.2.121.tgz",
|
||||
"integrity": "sha512-4XaGK+dRBYy7krln7BrDG0WsdE6ejUSgHjWHlUGXoubFfZUvls4GSahLcYjJBArLi4dLnxKw8zEuiQguPAIbrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -125,15 +119,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.2.119.tgz",
|
||||
"integrity": "sha512-9ePt4ZN+hsqDw4AgS4KtcWIGKfL9Oq28kwkrTER/QAcSrVKxiLonp81cCLzg7Ok/IUJu4Cfd71GZbFv/WE54zw==",
|
||||
"version": "0.2.121",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.2.121.tgz",
|
||||
"integrity": "sha512-DJUgpm7au086WaQV/S7BGOt2M8D90spGZRizT3twYsacf1BxzK1qsXqB/Pw1lUjPy6pI107pml/TaPzWuS/Vzg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -141,15 +132,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.2.119.tgz",
|
||||
"integrity": "sha512-QYxFNAe4FFridPkKhGlNcNBJ0TaIygWYyvfI9g4kX0i+RVbresUWuZVkWY06ioJ0fXoixFJ+HNQBMB7dLrIp8Q==",
|
||||
"version": "0.2.121",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.2.121.tgz",
|
||||
"integrity": "sha512-sQoGIgzLlBRrwizxsCV/lbaEuxXom/cfOwlDtQ2HnS1IzDDSjSf5d5pugpWItkOyXBWcHzMUu731WTTutvd/BQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -157,9 +145,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.2.119.tgz",
|
||||
"integrity": "sha512-p/TjcKQvkCYtXGPlR+mdyNwqCmvRcQL34Wtq0yUZ+iqmI/eyCe59IJ3AZrE0EZoqmiAevEYzatPIt9sncC9uxw==",
|
||||
"version": "0.2.121",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.2.121.tgz",
|
||||
"integrity": "sha512-6n/NHkHxs0/lCJX3XPADjo1EFzXBf0IwYz/nyzJGBCDJjGKmgTe0i8eYBr/hviwt1/OPeK7dmVzVSVl6EL9Azg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -170,9 +158,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.2.119.tgz",
|
||||
"integrity": "sha512-k98Ju0wtktm6FhqTE/cXlVr6K4kGqBolVjEGzeKkW6ZILc7124euwNapAvkQCwMAavAxS/ZnO3jdKMtHtwTVTA==",
|
||||
"version": "0.2.121",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.2.121.tgz",
|
||||
"integrity": "sha512-v2/R918/t94cCwc6rmbxk+UYeQPtF2oBLtQAk+cT0M60hvqmCZO2noyZx5uTp8TQncOlG4MkINIeNY2yfmWSoQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -662,9 +650,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -682,9 +667,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -702,9 +684,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -722,9 +701,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -742,9 +718,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -762,9 +735,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -947,17 +917,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz",
|
||||
"integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
|
||||
"integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.0",
|
||||
"@typescript-eslint/type-utils": "8.59.0",
|
||||
"@typescript-eslint/utils": "8.59.0",
|
||||
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/type-utils": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -970,22 +940,22 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz",
|
||||
"integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz",
|
||||
"integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1001,14 +971,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz",
|
||||
"integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz",
|
||||
"integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.0",
|
||||
"@typescript-eslint/types": "^8.59.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.1",
|
||||
"@typescript-eslint/types": "^8.59.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1023,14 +993,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz",
|
||||
"integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
|
||||
"integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/visitor-keys": "8.59.0"
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1041,9 +1011,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz",
|
||||
"integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz",
|
||||
"integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1058,15 +1028,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz",
|
||||
"integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz",
|
||||
"integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||
"@typescript-eslint/utils": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -1083,9 +1053,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz",
|
||||
"integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
|
||||
"integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1097,16 +1067,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz",
|
||||
"integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
|
||||
"integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.59.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||
"@typescript-eslint/project-service": "8.59.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -1125,16 +1095,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz",
|
||||
"integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz",
|
||||
"integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/typescript-estree": "8.59.0"
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1149,13 +1119,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz",
|
||||
"integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
|
||||
"integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1672,9 +1642,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
||||
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
|
||||
"integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2031,9 +2001,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.0.tgz",
|
||||
"integrity": "sha512-gDK8yiqKxrGta+3WtON59arrrw6GLmadA1qoFgYXzdcch8fmKDID2XqO8itsi3f1wufXYPT51387dN6cvVBS3Q==",
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
|
||||
"integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.1.0"
|
||||
@@ -2440,9 +2410,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
|
||||
"integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
||||
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
@@ -2654,9 +2624,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2678,9 +2645,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2702,9 +2666,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2726,9 +2687,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3107,9 +3065,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||
"version": "8.5.12",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
|
||||
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
10
package.json
10
package.json
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.31.1",
|
||||
"version": "0.31.4",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
@@ -60,16 +60,16 @@
|
||||
"author": "Zed Industries",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.20.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.119",
|
||||
"@agentclientprotocol/sdk": "0.21.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.121",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.89.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@types/node": "25.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.1",
|
||||
"@typescript-eslint/parser": "8.59.1",
|
||||
"eslint": "10.2.1",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.5.0",
|
||||
|
||||
274
src/acp-agent.ts
274
src/acp-agent.ts
@@ -385,29 +385,36 @@ const PERMISSION_MODE_ALIASES: Record<string, PermissionMode> = {
|
||||
bypass: "bypassPermissions",
|
||||
};
|
||||
|
||||
export function resolvePermissionMode(defaultMode?: unknown): PermissionMode {
|
||||
export function resolvePermissionMode(
|
||||
defaultMode?: unknown,
|
||||
logger: Logger = console,
|
||||
): PermissionMode {
|
||||
if (defaultMode === undefined) {
|
||||
return "default";
|
||||
}
|
||||
|
||||
if (typeof defaultMode !== "string") {
|
||||
throw new Error("Invalid permissions.defaultMode: expected a string.");
|
||||
logger.error("Ignoring permissions.defaultMode from settings: expected a string.");
|
||||
return "default";
|
||||
}
|
||||
|
||||
const normalized = defaultMode.trim().toLowerCase();
|
||||
if (normalized === "") {
|
||||
throw new Error("Invalid permissions.defaultMode: expected a non-empty string.");
|
||||
logger.error("Ignoring permissions.defaultMode from settings: expected a non-empty string.");
|
||||
return "default";
|
||||
}
|
||||
|
||||
const mapped = PERMISSION_MODE_ALIASES[normalized];
|
||||
if (!mapped) {
|
||||
throw new Error(`Invalid permissions.defaultMode: ${defaultMode}.`);
|
||||
logger.error(`Ignoring permissions.defaultMode from settings: unknown value '${defaultMode}'.`);
|
||||
return "default";
|
||||
}
|
||||
|
||||
if (mapped === "bypassPermissions" && !ALLOW_BYPASS) {
|
||||
throw new Error(
|
||||
"Invalid permissions.defaultMode: bypassPermissions is not available when running as root.",
|
||||
logger.error(
|
||||
"Ignoring permissions.defaultMode from settings: bypassPermissions is not available when running as root.",
|
||||
);
|
||||
return "default";
|
||||
}
|
||||
|
||||
return mapped;
|
||||
@@ -1302,7 +1309,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
// effort changes and effort changes induced by a model switch go through
|
||||
// the same path.
|
||||
|
||||
await this.applyConfigOptionValue(session, params.configId, resolvedValue);
|
||||
await this.applyConfigOptionValue(params.sessionId, session, params.configId, resolvedValue);
|
||||
|
||||
return { configOptions: session.configOptions };
|
||||
}
|
||||
@@ -1319,8 +1326,17 @@ export class ClaudeAcpAgent implements Agent {
|
||||
default:
|
||||
throw new Error("Invalid Mode");
|
||||
}
|
||||
|
||||
const session = this.sessions[sessionId];
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
if (!session.modes.availableModes.some((mode) => mode.id === modeId)) {
|
||||
throw new Error(`Mode ${modeId} is not available in this session`);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sessions[sessionId].query.setPermissionMode(modeId);
|
||||
await session.query.setPermissionMode(modeId);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (!error.message) {
|
||||
@@ -1390,7 +1406,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
|
||||
if (toolName === "ExitPlanMode") {
|
||||
const options = [
|
||||
const optionsAll = [
|
||||
{ kind: "allow_always", name: 'Yes, and use "auto" mode', optionId: "auto" },
|
||||
{
|
||||
kind: "allow_always",
|
||||
@@ -1401,12 +1417,20 @@ export class ClaudeAcpAgent implements Agent {
|
||||
{ kind: "reject_once", name: "No, keep planning", optionId: "plan" },
|
||||
];
|
||||
if (ALLOW_BYPASS) {
|
||||
options.unshift({
|
||||
optionsAll.unshift({
|
||||
kind: "allow_always",
|
||||
name: "Yes, and bypass permissions",
|
||||
optionId: "bypassPermissions",
|
||||
});
|
||||
}
|
||||
// Filter against the session's currently-advertised modes so we never
|
||||
// present options the active model can't honor (e.g. `auto` on Haiku).
|
||||
// `bypassPermissions` is already covered by `availableModes` via
|
||||
// `buildAvailableModes`/`ALLOW_BYPASS`. The `plan` option is a
|
||||
// "keep planning" reject path; it's always present in `availableModes`.
|
||||
const options = optionsAll.filter((o) =>
|
||||
session.modes.availableModes.some((m) => m.id === o.optionId),
|
||||
);
|
||||
|
||||
const response = await this.client.requestPermission({
|
||||
options,
|
||||
@@ -1425,27 +1449,30 @@ export class ClaudeAcpAgent implements Agent {
|
||||
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
||||
throw new Error("Tool use aborted");
|
||||
}
|
||||
const selectedMode =
|
||||
response.outcome?.outcome === "selected" ? response.outcome.optionId : undefined;
|
||||
const selectedModeWasOffered = options.some((option) => option.optionId === selectedMode);
|
||||
if (
|
||||
response.outcome?.outcome === "selected" &&
|
||||
(response.outcome.optionId === "default" ||
|
||||
response.outcome.optionId === "acceptEdits" ||
|
||||
response.outcome.optionId === "auto" ||
|
||||
response.outcome.optionId === "bypassPermissions")
|
||||
selectedModeWasOffered &&
|
||||
(selectedMode === "default" ||
|
||||
selectedMode === "acceptEdits" ||
|
||||
selectedMode === "auto" ||
|
||||
selectedMode === "bypassPermissions")
|
||||
) {
|
||||
await this.client.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "current_mode_update",
|
||||
currentModeId: response.outcome.optionId,
|
||||
currentModeId: selectedMode,
|
||||
},
|
||||
});
|
||||
await this.updateConfigOption(sessionId, "mode", response.outcome.optionId);
|
||||
await this.updateConfigOption(sessionId, "mode", selectedMode);
|
||||
|
||||
return {
|
||||
behavior: "allow",
|
||||
updatedInput: toolInput,
|
||||
updatedPermissions: suggestions ?? [
|
||||
{ type: "setMode", mode: response.outcome.optionId, destination: "session" },
|
||||
{ type: "setMode", mode: selectedMode, destination: "session" },
|
||||
],
|
||||
};
|
||||
} else {
|
||||
@@ -1543,7 +1570,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
const session = this.sessions[sessionId];
|
||||
if (!session) return;
|
||||
|
||||
await this.applyConfigOptionValue(session, configId, value);
|
||||
await this.applyConfigOptionValue(sessionId, session, configId, value);
|
||||
|
||||
await this.client.sessionUpdate({
|
||||
sessionId,
|
||||
@@ -1555,13 +1582,16 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
|
||||
private async applyConfigOptionValue(
|
||||
sessionId: string,
|
||||
session: Session,
|
||||
configId: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
// Sync top-level session state
|
||||
if (configId === "mode") {
|
||||
session.modes = { ...session.modes, currentModeId: value };
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
|
||||
);
|
||||
} else if (configId === "model") {
|
||||
if (session.models.currentModelId !== value) {
|
||||
// The cached context window was learned for the previous model; reset
|
||||
@@ -1571,10 +1601,38 @@ export class ClaudeAcpAgent implements Agent {
|
||||
session.contextWindowSize = inferContextWindowFromModel(value) ?? DEFAULT_CONTEXT_WINDOW;
|
||||
}
|
||||
session.models = { ...session.models, currentModelId: value };
|
||||
}
|
||||
|
||||
// Update configOptions
|
||||
if (configId === "model") {
|
||||
// Recompute availableModes for the new model and clamp the current
|
||||
// mode if the SDK no longer offers it (today: "auto" on Haiku).
|
||||
// `ModelInfo.supportsAutoMode` is the canonical SDK signal.
|
||||
const newModelInfo = session.modelInfos.find((m) => m.value === value);
|
||||
const newAvailableModes = buildAvailableModes(newModelInfo);
|
||||
// Capture BEFORE mutating session.modes so the log message reflects
|
||||
// the invalidated mode rather than "default".
|
||||
const previousModeId = session.modes.currentModeId;
|
||||
let modeDowngraded = false;
|
||||
if (!newAvailableModes.some((m) => m.id === previousModeId)) {
|
||||
session.modes = {
|
||||
availableModes: newAvailableModes,
|
||||
currentModeId: "default",
|
||||
};
|
||||
try {
|
||||
await session.query.setPermissionMode("default");
|
||||
} catch (err) {
|
||||
// Failing the entire model switch over a bookkeeping sync error is
|
||||
// worse UX than logging and continuing; the user explicitly asked
|
||||
// to change models. The next setPermissionMode from the user will
|
||||
// either succeed or surface a fresh error.
|
||||
this.logger.error(
|
||||
`Failed to sync permissionMode to "default" after model switch invalidated "${previousModeId}":`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
modeDowngraded = true;
|
||||
} else {
|
||||
session.modes = { ...session.modes, availableModes: newAvailableModes };
|
||||
}
|
||||
|
||||
// Rebuild config options since effort levels depend on the selected model
|
||||
const effortOpt = session.configOptions.find((o) => o.id === "effort");
|
||||
const currentEffort =
|
||||
@@ -1595,6 +1653,22 @@ export class ClaudeAcpAgent implements Agent {
|
||||
effortLevel: newEffort as Settings["effortLevel"],
|
||||
});
|
||||
}
|
||||
|
||||
// Emit current_mode_update only after session.modes AND
|
||||
// session.configOptions have been fully reconciled. This way, a failure
|
||||
// in the configOptions/effort rebuild above can't leave the client with
|
||||
// a clamped currentModeId but stale configOptions, and the notification
|
||||
// still precedes the caller's config_option_update so order-sensitive
|
||||
// clients update currentModeId before re-rendering the option list.
|
||||
if (modeDowngraded) {
|
||||
await this.client.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "current_mode_update",
|
||||
currentModeId: "default",
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
|
||||
@@ -1720,6 +1794,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
|
||||
const permissionMode = resolvePermissionMode(
|
||||
settingsManager.getSettings().permissions?.defaultMode,
|
||||
this.logger,
|
||||
);
|
||||
|
||||
// Extract options from _meta if provided
|
||||
@@ -1858,46 +1933,52 @@ export class ClaudeAcpAgent implements Agent {
|
||||
);
|
||||
}
|
||||
|
||||
const models = await getAvailableModels(q, initializationResult.models, settingsManager);
|
||||
const models = await getAvailableModels(
|
||||
q,
|
||||
initializationResult.models,
|
||||
settingsManager,
|
||||
this.logger,
|
||||
);
|
||||
|
||||
const availableModes = [
|
||||
{
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
description: "Use a model classifier to approve/deny permission prompts",
|
||||
},
|
||||
{
|
||||
id: "default",
|
||||
name: "Default",
|
||||
description: "Standard behavior, prompts for dangerous operations",
|
||||
},
|
||||
{
|
||||
id: "acceptEdits",
|
||||
name: "Accept Edits",
|
||||
description: "Auto-accept file edit operations",
|
||||
},
|
||||
{
|
||||
id: "plan",
|
||||
name: "Plan Mode",
|
||||
description: "Planning mode, no actual tool execution",
|
||||
},
|
||||
{
|
||||
id: "dontAsk",
|
||||
name: "Don't Ask",
|
||||
description: "Don't prompt for permissions, deny if not pre-approved",
|
||||
},
|
||||
];
|
||||
// Only works in non-root mode
|
||||
if (ALLOW_BYPASS) {
|
||||
availableModes.push({
|
||||
id: "bypassPermissions",
|
||||
name: "Bypass Permissions",
|
||||
description: "Bypass all permission checks",
|
||||
});
|
||||
// Gate `auto` (and future model-specific modes) on the resolved model's
|
||||
// `ModelInfo`. See `buildAvailableModes` for the canonical SDK signal.
|
||||
const currentModelInfo = initializationResult.models.find(
|
||||
(m) => m.value === models.currentModelId,
|
||||
);
|
||||
const availableModes = buildAvailableModes(currentModelInfo);
|
||||
|
||||
// Clamp `permissionMode` if the resolved session does not offer it. The
|
||||
// common case is `permissions.defaultMode: "auto"` resolving to a model
|
||||
// that does not support auto mode (e.g. Haiku); without this clamp the
|
||||
// SDK would later throw `"auto mode unavailable for this model"` from
|
||||
// `setPermissionMode`. Keep `permissionMode` as the resolved user intent
|
||||
// (matches what was passed into `options.permissionMode` above) and use
|
||||
// `effectiveMode` for the post-clamp value the session actually runs in.
|
||||
let effectiveMode: PermissionMode = permissionMode;
|
||||
if (!availableModes.some((m) => m.id === effectiveMode)) {
|
||||
if (effectiveMode === "auto") {
|
||||
this.logger.error(
|
||||
`permissions.defaultMode "auto" is not available for model ` +
|
||||
`"${models.currentModelId}"; falling back to "default".`,
|
||||
);
|
||||
} else {
|
||||
this.logger.error(
|
||||
`permissions.defaultMode "${effectiveMode}" is not available in ` +
|
||||
`this session; falling back to "default".`,
|
||||
);
|
||||
}
|
||||
effectiveMode = "default";
|
||||
// Sync the SDK so it doesn't keep "auto" cached internally. Wrapped in
|
||||
// try/catch since failing here would abort session creation entirely.
|
||||
try {
|
||||
await q.setPermissionMode("default");
|
||||
} catch (err) {
|
||||
this.logger.error("Failed to sync clamped permissionMode to SDK:", err);
|
||||
}
|
||||
}
|
||||
|
||||
const modes = {
|
||||
currentModeId: permissionMode,
|
||||
currentModeId: effectiveMode,
|
||||
availableModes,
|
||||
};
|
||||
|
||||
@@ -2040,6 +2121,58 @@ function createEnvForGateway(gatewayMeta?: GatewayAuthMeta) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the list of permission modes the agent will advertise for the given
|
||||
* model. `auto` is gated by `ModelInfo.supportsAutoMode === true`, which is
|
||||
* the SDK's model-level availability signal. `undefined`/`false` both exclude
|
||||
* `auto`. `bypassPermissions` is still gated by `ALLOW_BYPASS`.
|
||||
*/
|
||||
function buildAvailableModes(modelInfo: ModelInfo | undefined): SessionModeState["availableModes"] {
|
||||
const modes: SessionModeState["availableModes"] = [];
|
||||
|
||||
// Only advertise "auto" when the SDK reports the model supports it.
|
||||
if (modelInfo?.supportsAutoMode === true) {
|
||||
modes.push({
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
description: "Use a model classifier to approve/deny permission prompts",
|
||||
});
|
||||
}
|
||||
|
||||
modes.push(
|
||||
{
|
||||
id: "default",
|
||||
name: "Default",
|
||||
description: "Standard behavior, prompts for dangerous operations",
|
||||
},
|
||||
{
|
||||
id: "acceptEdits",
|
||||
name: "Accept Edits",
|
||||
description: "Auto-accept file edit operations",
|
||||
},
|
||||
{
|
||||
id: "plan",
|
||||
name: "Plan Mode",
|
||||
description: "Planning mode, no actual tool execution",
|
||||
},
|
||||
{
|
||||
id: "dontAsk",
|
||||
name: "Don't Ask",
|
||||
description: "Don't prompt for permissions, deny if not pre-approved",
|
||||
},
|
||||
);
|
||||
|
||||
if (ALLOW_BYPASS) {
|
||||
modes.push({
|
||||
id: "bypassPermissions",
|
||||
name: "Bypass Permissions",
|
||||
description: "Bypass all permission checks",
|
||||
});
|
||||
}
|
||||
|
||||
return modes;
|
||||
}
|
||||
|
||||
function buildConfigOptions(
|
||||
modes: SessionModeState,
|
||||
models: SessionModelState,
|
||||
@@ -2106,7 +2239,7 @@ function buildConfigOptions(
|
||||
id: "effort",
|
||||
name: "Effort",
|
||||
description: "Available effort levels for this model",
|
||||
category: "effort",
|
||||
category: "thought_level",
|
||||
type: "select",
|
||||
currentValue: validEffort,
|
||||
options: effortOptions,
|
||||
@@ -2189,10 +2322,27 @@ function resolveModelPreference(models: ModelInfo[], preference: string): ModelI
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
function resolveSettingsModel(
|
||||
models: ModelInfo[],
|
||||
settingsModel: unknown,
|
||||
logger: Logger,
|
||||
): ModelInfo | null {
|
||||
if (settingsModel === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (typeof settingsModel !== "string") {
|
||||
const typeLabel = settingsModel === null ? "null" : typeof settingsModel;
|
||||
logger.error(`Ignoring model from settings: expected a string, got ${typeLabel}.`);
|
||||
return null;
|
||||
}
|
||||
return resolveModelPreference(models, settingsModel);
|
||||
}
|
||||
|
||||
async function getAvailableModels(
|
||||
query: Query,
|
||||
models: ModelInfo[],
|
||||
settingsManager: SettingsManager,
|
||||
logger: Logger,
|
||||
): Promise<SessionModelState> {
|
||||
const settings = settingsManager.getSettings();
|
||||
|
||||
@@ -2207,8 +2357,8 @@ async function getAvailableModels(
|
||||
if (match) {
|
||||
currentModel = match;
|
||||
}
|
||||
} else if (settings.model) {
|
||||
const match = resolveModelPreference(models, settings.model);
|
||||
} else {
|
||||
const match = resolveSettingsModel(models, settings.model, logger);
|
||||
if (match) {
|
||||
currentModel = match;
|
||||
}
|
||||
|
||||
@@ -145,12 +145,12 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
expect(response.modes.currentModeId).toBe("default");
|
||||
});
|
||||
|
||||
it("throws when permissions.defaultMode is not a string", async () => {
|
||||
it("falls back to 'default' when permissions.defaultMode is invalid", async () => {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
defaultMode: 123,
|
||||
defaultMode: "not-a-real-mode",
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -158,18 +158,208 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
mockQuery();
|
||||
const { getCapturedOptions } = mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
await expect(
|
||||
(agent as any).createSession({
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
// Bad mode is ignored at the usage site; session creation must not throw.
|
||||
expect(getCapturedOptions().permissionMode).toBe("default");
|
||||
expect(response.modes.currentModeId).toBe("default");
|
||||
});
|
||||
|
||||
it("ignores model from settings when it is not a string", async () => {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
model: 123,
|
||||
}),
|
||||
);
|
||||
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
const { setModelSpy } = mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
// Bad model is ignored at the usage site; falls back to the first SDK model.
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-sonnet-4-5");
|
||||
expect(response.models.currentModelId).toBe("claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
describe("auto mode availability per model", () => {
|
||||
function mockQueryWithModels(models: any[]): {
|
||||
getCapturedOptions: () => any;
|
||||
setModelSpy: ReturnType<typeof vi.fn>;
|
||||
setPermissionModeSpy: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
let capturedOptions: any;
|
||||
const setModelSpy = vi.fn();
|
||||
const setPermissionModeSpy = vi.fn();
|
||||
querySpy.mockImplementation(({ options }: any) => {
|
||||
capturedOptions = options;
|
||||
return {
|
||||
initializationResult: async () => ({ models }),
|
||||
setModel: setModelSpy,
|
||||
setPermissionMode: setPermissionModeSpy,
|
||||
supportedCommands: async () => [],
|
||||
} as any;
|
||||
});
|
||||
return {
|
||||
getCapturedOptions: () => capturedOptions,
|
||||
setModelSpy,
|
||||
setPermissionModeSpy,
|
||||
};
|
||||
}
|
||||
|
||||
it("omits `auto` from availableModes when the resolved model lacks supportsAutoMode", async () => {
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
mockQueryWithModels([
|
||||
{
|
||||
value: "claude-haiku-4-5",
|
||||
displayName: "Claude Haiku",
|
||||
description: "Fast",
|
||||
// supportsAutoMode intentionally omitted
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
}),
|
||||
).rejects.toThrow("Invalid permissions.defaultMode");
|
||||
});
|
||||
|
||||
const modeIds: string[] = response.modes.availableModes.map((m: any) => m.id);
|
||||
expect(modeIds).not.toContain("auto");
|
||||
expect(modeIds).toEqual(
|
||||
expect.arrayContaining(["default", "acceptEdits", "plan", "dontAsk"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("includes `auto` when the resolved model has supportsAutoMode: true", async () => {
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
mockQueryWithModels([
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
displayName: "Claude Opus",
|
||||
description: "Most capable",
|
||||
supportsAutoMode: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
const modeIds: string[] = response.modes.availableModes.map((m: any) => m.id);
|
||||
expect(modeIds).toContain("auto");
|
||||
});
|
||||
|
||||
it("clamps permissions.defaultMode='auto' to 'default' on a model that lacks supportsAutoMode", async () => {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({ permissions: { defaultMode: "auto" } }),
|
||||
);
|
||||
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
const { getCapturedOptions, setPermissionModeSpy } = mockQueryWithModels([
|
||||
{
|
||||
value: "claude-haiku-4-5",
|
||||
displayName: "Claude Haiku",
|
||||
description: "Fast",
|
||||
// supportsAutoMode intentionally omitted
|
||||
},
|
||||
]);
|
||||
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
// Options.permissionMode is built before init resolves, so it still
|
||||
// carries the user-typed value; the SDK was synced via
|
||||
// setPermissionMode after we discovered the model can't honor it.
|
||||
expect(getCapturedOptions().permissionMode).toBe("auto");
|
||||
expect(setPermissionModeSpy).toHaveBeenCalledWith("default");
|
||||
expect(response.modes.currentModeId).toBe("default");
|
||||
expect(response.modes.availableModes.map((m: any) => m.id)).not.toContain("auto");
|
||||
|
||||
// A descriptive warning was logged so operators see the clamp.
|
||||
const messages = errorSpy.mock.calls.map((c) => c.join(" "));
|
||||
expect(messages.some((m) => m.includes("auto") && m.includes("claude-haiku-4-5"))).toBe(
|
||||
true,
|
||||
);
|
||||
} finally {
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not clamp permissions.defaultMode='auto' on a model that supports auto", async () => {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({ permissions: { defaultMode: "auto" } }),
|
||||
);
|
||||
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
const { getCapturedOptions, setPermissionModeSpy } = mockQueryWithModels([
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
displayName: "Claude Opus",
|
||||
description: "Most capable",
|
||||
supportsAutoMode: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
expect(getCapturedOptions().permissionMode).toBe("auto");
|
||||
expect(setPermissionModeSpy).not.toHaveBeenCalled();
|
||||
expect(response.modes.currentModeId).toBe("auto");
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves model aliases like opus[1m] to the correct model", async () => {
|
||||
|
||||
@@ -2502,7 +2502,12 @@ describe("usage_update computation", () => {
|
||||
const session = agent.sessions["test-session"];
|
||||
expect(session.contextWindowSize).toBe(200000);
|
||||
|
||||
await (agent as any).applyConfigOptionValue(session, "model", "claude-opus-4-6-1m");
|
||||
await (agent as any).applyConfigOptionValue(
|
||||
"test-session",
|
||||
session,
|
||||
"model",
|
||||
"claude-opus-4-6-1m",
|
||||
);
|
||||
expect(session.contextWindowSize).toBe(1000000);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "test" }] });
|
||||
@@ -2587,7 +2592,12 @@ describe("usage_update computation", () => {
|
||||
session.models = { ...session.models, currentModelId: "claude-opus-4-6-1m" };
|
||||
|
||||
// User flips the selector to a 200k model.
|
||||
await (agent as any).applyConfigOptionValue(session, "model", "claude-sonnet-4-6");
|
||||
await (agent as any).applyConfigOptionValue(
|
||||
"test-session",
|
||||
session,
|
||||
"model",
|
||||
"claude-sonnet-4-6",
|
||||
);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "test" }] });
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { resolvePermissionMode } from "../acp-agent.js";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { resolvePermissionMode, type Logger } from "../acp-agent.js";
|
||||
|
||||
function mockLogger() {
|
||||
const error = vi.fn<(...args: any[]) => void>();
|
||||
const log = vi.fn<(...args: any[]) => void>();
|
||||
const logger: Logger = { log, error };
|
||||
return { logger, error, log };
|
||||
}
|
||||
|
||||
describe("resolvePermissionMode", () => {
|
||||
it("returns 'default' when no mode is provided", () => {
|
||||
@@ -26,18 +33,25 @@ describe("resolvePermissionMode", () => {
|
||||
expect(resolvePermissionMode(" dontAsk ")).toBe("dontAsk");
|
||||
});
|
||||
|
||||
it("throws on non-string values", () => {
|
||||
expect(() => resolvePermissionMode(123)).toThrow("expected a string");
|
||||
expect(() => resolvePermissionMode(true)).toThrow("expected a string");
|
||||
expect(() => resolvePermissionMode({})).toThrow("expected a string");
|
||||
it("falls back to 'default' and logs on non-string values", () => {
|
||||
for (const value of [123, true, {}]) {
|
||||
const { logger, error } = mockLogger();
|
||||
expect(resolvePermissionMode(value, logger)).toBe("default");
|
||||
expect(error).toHaveBeenCalledWith(expect.stringContaining("expected a string"));
|
||||
}
|
||||
});
|
||||
|
||||
it("throws on empty string", () => {
|
||||
expect(() => resolvePermissionMode("")).toThrow("expected a non-empty string");
|
||||
expect(() => resolvePermissionMode(" ")).toThrow("expected a non-empty string");
|
||||
it("falls back to 'default' and logs on empty string", () => {
|
||||
for (const value of ["", " "]) {
|
||||
const { logger, error } = mockLogger();
|
||||
expect(resolvePermissionMode(value, logger)).toBe("default");
|
||||
expect(error).toHaveBeenCalledWith(expect.stringContaining("expected a non-empty string"));
|
||||
}
|
||||
});
|
||||
|
||||
it("throws on unknown mode", () => {
|
||||
expect(() => resolvePermissionMode("yolo")).toThrow("Invalid permissions.defaultMode: yolo");
|
||||
it("falls back to 'default' and logs on unknown mode", () => {
|
||||
const { logger, error } = mockLogger();
|
||||
expect(resolvePermissionMode("yolo", logger)).toBe("default");
|
||||
expect(error).toHaveBeenCalledWith(expect.stringContaining("yolo"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -811,4 +811,391 @@ describe("session config options", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("auto mode availability per model", () => {
|
||||
/**
|
||||
* Augment the session populated by `populateSession()` with a Haiku entry
|
||||
* (no `supportsAutoMode`), Opus + Sonnet entries with `supportsAutoMode:
|
||||
* true`, and seed `availableModes` so it currently includes `auto`. This
|
||||
* exercises the per-model recomputation done by `applyConfigOptionValue`
|
||||
* on a model switch.
|
||||
*/
|
||||
function setupHaikuOpusSession(currentModeId: string = "default") {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
session.modelInfos = [
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
displayName: "Claude Opus",
|
||||
description: "Most capable",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
supportsAutoMode: true,
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
supportsAutoMode: true,
|
||||
},
|
||||
{
|
||||
value: "claude-haiku-4-5",
|
||||
displayName: "Claude Haiku",
|
||||
description: "Fast",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
// supportsAutoMode intentionally omitted
|
||||
},
|
||||
];
|
||||
session.models = {
|
||||
currentModelId: "claude-opus-4-5",
|
||||
availableModels: [
|
||||
{ modelId: "claude-opus-4-5", name: "Claude Opus", description: "Most capable" },
|
||||
{ modelId: "claude-sonnet-4-5", name: "Claude Sonnet", description: "Balanced" },
|
||||
{ modelId: "claude-haiku-4-5", name: "Claude Haiku", description: "Fast" },
|
||||
],
|
||||
};
|
||||
session.modes = {
|
||||
currentModeId,
|
||||
availableModes: [
|
||||
{
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
description: "Use a model classifier to approve/deny permission prompts",
|
||||
},
|
||||
{
|
||||
id: "default",
|
||||
name: "Default",
|
||||
description: "Standard behavior, prompts for dangerous operations",
|
||||
},
|
||||
{
|
||||
id: "acceptEdits",
|
||||
name: "Accept Edits",
|
||||
description: "Auto-accept file edit operations",
|
||||
},
|
||||
{ id: "plan", name: "Plan Mode", description: "Planning mode" },
|
||||
{
|
||||
id: "dontAsk",
|
||||
name: "Don't Ask",
|
||||
description: "Don't prompt for permissions, deny if not pre-approved",
|
||||
},
|
||||
],
|
||||
};
|
||||
// Reflect the seeded availableModes/availableModels in configOptions so
|
||||
// the pre-state matches what `createSession` would have produced for
|
||||
// Opus, and `setSessionConfigOption` validation can accept the seeded
|
||||
// model ids (notably the new Haiku entry).
|
||||
session.configOptions = session.configOptions.map((o: any) => {
|
||||
if (o.id === "mode") {
|
||||
return {
|
||||
...o,
|
||||
currentValue: currentModeId,
|
||||
options: session.modes.availableModes.map((m: any) => ({
|
||||
value: m.id,
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (o.id === "model") {
|
||||
return {
|
||||
...o,
|
||||
currentValue: session.models.currentModelId,
|
||||
options: session.models.availableModels.map((m: any) => ({
|
||||
value: m.modelId,
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return o;
|
||||
});
|
||||
return session;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
populateSession();
|
||||
});
|
||||
|
||||
it("drops `auto` from available modes when switching to Haiku", async () => {
|
||||
setupHaikuOpusSession("default");
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-haiku-4-5",
|
||||
});
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
expect(configUpdate).toBeDefined();
|
||||
const modeOption = (configUpdate?.update as any).configOptions.find(
|
||||
(o: any) => o.id === "mode",
|
||||
);
|
||||
expect(modeOption).toBeDefined();
|
||||
const modeValues = modeOption.options.map((o: any) => o.value);
|
||||
expect(modeValues).not.toContain("auto");
|
||||
expect(modeValues).toEqual(
|
||||
expect.arrayContaining(["default", "acceptEdits", "plan", "dontAsk"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("clamps to `default` and emits current_mode_update when Opus(auto) → Haiku", async () => {
|
||||
setupHaikuOpusSession("auto");
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-haiku-4-5",
|
||||
});
|
||||
|
||||
// SDK was synced to "default".
|
||||
expect(setPermissionModeSpy).toHaveBeenCalledWith("default");
|
||||
|
||||
// current_mode_update was emitted before config_option_update so a
|
||||
// client applying notifications in order observes the mode change
|
||||
// before re-rendering the config-option list.
|
||||
const modeUpdateIdx = sessionUpdates.findIndex(
|
||||
(n) => n.update.sessionUpdate === "current_mode_update",
|
||||
);
|
||||
const configUpdateIdx = sessionUpdates.findIndex(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
expect(modeUpdateIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(configUpdateIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(modeUpdateIdx).toBeLessThan(configUpdateIdx);
|
||||
expect((sessionUpdates[modeUpdateIdx].update as any).currentModeId).toBe("default");
|
||||
|
||||
// configOptions reflect the clamped mode.
|
||||
const modeOption = (sessionUpdates[configUpdateIdx].update as any).configOptions.find(
|
||||
(o: any) => o.id === "mode",
|
||||
);
|
||||
expect(modeOption.currentValue).toBe("default");
|
||||
});
|
||||
|
||||
it("re-adds `auto` when switching from Haiku back to Opus", async () => {
|
||||
const session = setupHaikuOpusSession("default");
|
||||
// Pretend Haiku is the current model with no `auto`.
|
||||
session.models.currentModelId = "claude-haiku-4-5";
|
||||
session.modes.availableModes = session.modes.availableModes.filter(
|
||||
(m: any) => m.id !== "auto",
|
||||
);
|
||||
const modeOpt = session.configOptions.find((o: any) => o.id === "mode");
|
||||
modeOpt.options = session.modes.availableModes.map((m: any) => ({
|
||||
value: m.id,
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
}));
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-opus-4-5",
|
||||
});
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
expect(configUpdate).toBeDefined();
|
||||
const modeOption = (configUpdate?.update as any).configOptions.find(
|
||||
(o: any) => o.id === "mode",
|
||||
);
|
||||
const modeValues = modeOption.options.map((o: any) => o.value);
|
||||
expect(modeValues).toContain("auto");
|
||||
|
||||
// The current mode ("default") is still valid on Opus, so no
|
||||
// current_mode_update should have been emitted by the model switch.
|
||||
const modeUpdates = sessionUpdates.filter(
|
||||
(n) => n.update.sessionUpdate === "current_mode_update",
|
||||
);
|
||||
expect(modeUpdates).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("preserves the current mode when it remains valid after a model switch", async () => {
|
||||
setupHaikuOpusSession("plan");
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-haiku-4-5",
|
||||
});
|
||||
|
||||
// `plan` is in availableModes for both Opus and Haiku, so no clamp.
|
||||
expect(setPermissionModeSpy).not.toHaveBeenCalledWith("default");
|
||||
|
||||
const modeUpdates = sessionUpdates.filter(
|
||||
(n) => n.update.sessionUpdate === "current_mode_update",
|
||||
);
|
||||
expect(modeUpdates).toHaveLength(0);
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
const modeOption = (configUpdate?.update as any).configOptions.find(
|
||||
(o: any) => o.id === "mode",
|
||||
);
|
||||
expect(modeOption.currentValue).toBe("plan");
|
||||
});
|
||||
|
||||
it("clamps mode and emits current_mode_update via setSessionConfigOption(model)", async () => {
|
||||
// Mirrors the unstable_setSessionModel(auto → Haiku) test, but goes
|
||||
// through the request/response API. The `current_mode_update` side
|
||||
// effect must still fire so clients learn about the clamp regardless of
|
||||
// which entry point triggered the model switch.
|
||||
setupHaikuOpusSession("auto");
|
||||
|
||||
const response = await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-haiku-4-5",
|
||||
});
|
||||
|
||||
expect(setPermissionModeSpy).toHaveBeenCalledWith("default");
|
||||
|
||||
const modeUpdates = sessionUpdates.filter(
|
||||
(n) => n.update.sessionUpdate === "current_mode_update",
|
||||
);
|
||||
expect(modeUpdates).toHaveLength(1);
|
||||
expect((modeUpdates[0].update as any).currentModeId).toBe("default");
|
||||
|
||||
// setSessionConfigOption is a request/response API: it returns the new
|
||||
// configOptions in the response rather than emitting a
|
||||
// config_option_update notification.
|
||||
const configUpdates = sessionUpdates.filter(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
expect(configUpdates).toHaveLength(0);
|
||||
|
||||
const modeOption = response.configOptions.find((o: any) => o.id === "mode");
|
||||
expect(modeOption).toBeDefined();
|
||||
expect((modeOption as any).currentValue).toBe("default");
|
||||
expect((modeOption as any).options.map((o: any) => o.value)).not.toContain("auto");
|
||||
});
|
||||
|
||||
it("rejects direct setSessionMode to `auto` when the active model does not offer it", async () => {
|
||||
const session = setupHaikuOpusSession("default");
|
||||
session.models.currentModelId = "claude-haiku-4-5";
|
||||
session.modes.availableModes = session.modes.availableModes.filter(
|
||||
(mode: any) => mode.id !== "auto",
|
||||
);
|
||||
|
||||
await expect(agent.setSessionMode({ sessionId: SESSION_ID, modeId: "auto" })).rejects.toThrow(
|
||||
"Mode auto is not available in this session",
|
||||
);
|
||||
|
||||
expect(setPermissionModeSpy).not.toHaveBeenCalledWith("auto");
|
||||
expect(sessionUpdates).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExitPlanMode permission options filtered by availableModes", () => {
|
||||
let capturedPermissionRequest: any;
|
||||
let permissionResponse: any;
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPermissionRequest = null;
|
||||
permissionResponse = { outcome: { outcome: "cancelled" } };
|
||||
// Replace the default mock client with one that captures the
|
||||
// requestPermission call so we can assert on the offered options.
|
||||
(agent as any).client = {
|
||||
sessionUpdate: async (notification: SessionNotification) => {
|
||||
sessionUpdates.push(notification);
|
||||
},
|
||||
requestPermission: async (params: any) => {
|
||||
capturedPermissionRequest = params;
|
||||
return permissionResponse;
|
||||
},
|
||||
readTextFile: async () => ({ content: "" }),
|
||||
writeTextFile: async () => ({}),
|
||||
};
|
||||
populateSession();
|
||||
});
|
||||
|
||||
it("omits the `auto` option on a model without supportsAutoMode", async () => {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
// Haiku-shaped session: availableModes does NOT include `auto`.
|
||||
session.modes = {
|
||||
currentModeId: "plan",
|
||||
availableModes: [
|
||||
{ id: "default", name: "Default", description: "Standard" },
|
||||
{ id: "acceptEdits", name: "Accept Edits", description: "Auto-accept edits" },
|
||||
{ id: "plan", name: "Plan Mode", description: "Planning mode" },
|
||||
{ id: "dontAsk", name: "Don't Ask", description: "Deny if not pre-approved" },
|
||||
],
|
||||
};
|
||||
|
||||
const canUseTool = (agent as any).canUseTool(SESSION_ID);
|
||||
const signal = new AbortController().signal;
|
||||
try {
|
||||
await canUseTool(
|
||||
"ExitPlanMode",
|
||||
{ plan: "do stuff" },
|
||||
{ signal, suggestions: undefined, toolUseID: "toolu_1" },
|
||||
);
|
||||
} catch {
|
||||
// The mock client returns `cancelled`, which makes canUseTool throw.
|
||||
// We only care about the captured requestPermission options.
|
||||
}
|
||||
|
||||
expect(capturedPermissionRequest).not.toBeNull();
|
||||
const optionIds = capturedPermissionRequest.options.map((o: any) => o.optionId);
|
||||
expect(optionIds).not.toContain("auto");
|
||||
expect(optionIds).toEqual(expect.arrayContaining(["default", "acceptEdits", "plan"]));
|
||||
});
|
||||
|
||||
it("denies a selected `auto` option if the client did not receive that option", async () => {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
session.modes = {
|
||||
currentModeId: "plan",
|
||||
availableModes: [
|
||||
{ id: "default", name: "Default", description: "Standard" },
|
||||
{ id: "acceptEdits", name: "Accept Edits", description: "Auto-accept edits" },
|
||||
{ id: "plan", name: "Plan Mode", description: "Planning mode" },
|
||||
{ id: "dontAsk", name: "Don't Ask", description: "Deny if not pre-approved" },
|
||||
],
|
||||
};
|
||||
permissionResponse = { outcome: { outcome: "selected", optionId: "auto" } };
|
||||
|
||||
const canUseTool = (agent as any).canUseTool(SESSION_ID);
|
||||
const result = await canUseTool(
|
||||
"ExitPlanMode",
|
||||
{ plan: "do stuff" },
|
||||
{ signal: new AbortController().signal, suggestions: undefined, toolUseID: "toolu_2" },
|
||||
);
|
||||
|
||||
expect(capturedPermissionRequest).not.toBeNull();
|
||||
const optionIds = capturedPermissionRequest.options.map((o: any) => o.optionId);
|
||||
expect(optionIds).not.toContain("auto");
|
||||
expect(result.behavior).toBe("deny");
|
||||
expect(sessionUpdates).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("includes the `auto` option on a model with supportsAutoMode", async () => {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
session.modes = {
|
||||
currentModeId: "plan",
|
||||
availableModes: [
|
||||
{ id: "auto", name: "Auto", description: "Use a model classifier" },
|
||||
{ id: "default", name: "Default", description: "Standard" },
|
||||
{ id: "acceptEdits", name: "Accept Edits", description: "Auto-accept edits" },
|
||||
{ id: "plan", name: "Plan Mode", description: "Planning mode" },
|
||||
{ id: "dontAsk", name: "Don't Ask", description: "Deny if not pre-approved" },
|
||||
],
|
||||
};
|
||||
|
||||
const canUseTool = (agent as any).canUseTool(SESSION_ID);
|
||||
const signal = new AbortController().signal;
|
||||
try {
|
||||
await canUseTool(
|
||||
"ExitPlanMode",
|
||||
{ plan: "do stuff" },
|
||||
{ signal, suggestions: undefined, toolUseID: "toolu_3" },
|
||||
);
|
||||
} catch {
|
||||
// mock returns cancelled
|
||||
}
|
||||
|
||||
expect(capturedPermissionRequest).not.toBeNull();
|
||||
const optionIds = capturedPermissionRequest.options.map((o: any) => o.optionId);
|
||||
expect(optionIds).toContain("auto");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user