mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-09 19:02:05 +00:00
Compare commits
10 Commits
v0.101.0
...
refactor/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a39e511c97 | ||
|
|
a58293f4c9 | ||
|
|
3b22586cf6 | ||
|
|
47b64c04de | ||
|
|
1ee2422934 | ||
|
|
7d1ef76c1d | ||
|
|
aa0c1234b5 | ||
|
|
54392e144d | ||
|
|
6597626d86 | ||
|
|
ba162346c9 |
120
package-lock.json
generated
120
package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "2.4.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.318",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.319",
|
||||
"diff": "9.0.0",
|
||||
"zod": "4.6.7"
|
||||
},
|
||||
@@ -18,7 +18,7 @@
|
||||
"claude-agent-acp": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "1.4.0",
|
||||
"@anthropic-ai/sdk": "1.6.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tsconfig/node22": "22.0.6",
|
||||
"@types/node": "26.6.6",
|
||||
@@ -53,22 +53,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.3.318",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.318.tgz",
|
||||
"integrity": "sha512-z5WHCUeW52SKQ3rnvI3zt8X5h2QQ5o3Cgg5xarO5Rimfe67f3pOJt2aOh00S8t/nRzqfs+8nCb9o1EoXe+yccA==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.319.tgz",
|
||||
"integrity": "sha512-7X+sh4uZv/u4OL4I4cQvMr9N3NM6FDiuyk829hFylgbrnNwVyiGElWEDWGuO26zwMIxmgpKdPoDILoFIHh1uwA==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.14.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.318",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.318",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.318",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.318",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.318",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.318",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.318",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.318"
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.319"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@anthropic-ai/sdk": ">=0.93.0",
|
||||
@@ -77,9 +77,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
|
||||
"version": "0.3.318",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.318.tgz",
|
||||
"integrity": "sha512-AWoXeDXzAMD3RaA21lWuHvCrKd8xmbTTmKjovninn4j07jheoVSMzFbCFjpp4kk5TdhT36JtWPkAxvjNMbcO8w==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.319.tgz",
|
||||
"integrity": "sha512-rNiAbMbRhgOM2CELsDTIq7q++sFVIZukTkgeswcncGbcX8STIgtf0XaZzKdRXtKK+v9ez+mQn8P4pb2Ka17wGA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -90,9 +90,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
|
||||
"version": "0.3.318",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.318.tgz",
|
||||
"integrity": "sha512-6CkZ5PCdebSY/LePp16sf/HYRtM6zVuxtn8qYICFS5KUZFsjccJJ9swVsOdHXNHi4SWZcSmIFTwoUbfFCzisRw==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.319.tgz",
|
||||
"integrity": "sha512-jM730Fl+cFotDnjF9cStIf+Xe57YdfDB3TXaSVVv8uVAXW4xoWFwIj2eDvmsciEEe5UMkKzjVqXu9kR21OrXng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -103,9 +103,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
|
||||
"version": "0.3.318",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.318.tgz",
|
||||
"integrity": "sha512-mBm4su17aMitLBBCQj3bHdejyUDY6z/ormGih+HG24N689RSbdKECjGjkT87tDBIR/4jyoqmuSeHqM1DI8TJlg==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.319.tgz",
|
||||
"integrity": "sha512-g2tpMsHLMQahI4bk5CIcMHAB/RwzxoPGmL5Q5G5MDir5WTrl71vulWbtLQISUPm/yFPRG97tdzZnCa5Uq1v/dw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -119,9 +119,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
|
||||
"version": "0.3.318",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.318.tgz",
|
||||
"integrity": "sha512-l4LSYFOrvlYCrUImGfxMTQ0wg+T3s9CBhrBvPF/0oJGKZg+QnxHHU8jUVm9e9/xYmDRTtqFIrr4id/5AfH4XGw==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.319.tgz",
|
||||
"integrity": "sha512-8KDZQJAS2CEb5aGAvuXkpyHMc9TOM/zYTw2iKmbbVi4im0atw93EtJPapYyp0g/lQI/Xk8jMYdWU6Zlv9EWjLw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -135,9 +135,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
|
||||
"version": "0.3.318",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.318.tgz",
|
||||
"integrity": "sha512-+mE+ueBD3oRFdaG16rd0h+LMjdeivGxL0pr/+FZK7JeLWVu+yPSw/f/X02b9oK36/DWykiFTu2G6HnJ9/+YO6w==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.319.tgz",
|
||||
"integrity": "sha512-uBAZA8x0hd5gVifyL5Lo73dVmi9xZeJ6jro2NO0al6yyWaw/6UL9xbsVh9lOhLFOmYIdSbQ2j4zAfnvG/t5F3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -151,9 +151,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
|
||||
"version": "0.3.318",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.318.tgz",
|
||||
"integrity": "sha512-SAF3lj/2OzTU84p+OOA13QEKA9+5fYAw0M9E7AJjdnRmVFGqQfbV7Do1i1f9Sj0mIubB3rLTRphGRw8O4HZjgA==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.319.tgz",
|
||||
"integrity": "sha512-XkKYj3c1jVFjLATrZr4YTsmS5AIb5s2iphF9X670EcVQqDCCGIKKTVNLLg9mg1e7D+2xX8FU0XNlOJatWfpBeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -167,9 +167,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
|
||||
"version": "0.3.318",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.318.tgz",
|
||||
"integrity": "sha512-19iVSZ4379QbZU1XnGUtfFvhKS1WzS8H1sbU/DbrzqbJ5Kq1eB1drj+obB1fCxTh6m26tRPj0zMJmdeu/jk3Gw==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.319.tgz",
|
||||
"integrity": "sha512-YoP0lYHJCziENFCotHQq9qd/gaJrwUNkAcnZyVjXxg1OPmtKuyw31aG60s4cWXRDXXGm/m1siNSQaGGfAYBvrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -180,9 +180,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
|
||||
"version": "0.3.318",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.318.tgz",
|
||||
"integrity": "sha512-ZdKVWpyVNVt0cXrOf8sUBaxcxLEAP+oGMdQDLBWr/BqOxTWLy7TBWOj1kzw+wVg/9WIkjkgQWeJzGXPWulR1sA==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.319.tgz",
|
||||
"integrity": "sha512-UC27x+8gfCS2OjNGJWrueP0gLOOKsLEsJ/kg0p6aa8GToZcJYjJ8KeAapHHRYMUAIB6Sarqe49Itm5P1wswCcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -193,9 +193,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.4.0.tgz",
|
||||
"integrity": "sha512-tyuibnhQL57kLOx5oomY4Ld7jOVOPNC5v3IiyRtR3l+Friv66tI4VbuZYF2NMel/T0WMuQMBFOMKDBUbUh8gPA==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.6.0.tgz",
|
||||
"integrity": "sha512-XqiO/46n8S6So8jQ/6/uNCkHntyW6QHHs1gZ+hlwTx1fWDr929g4VmwIXfdMVk7SuBWNdQvAlwyi1G3t6SEIuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.4.1",
|
||||
@@ -215,9 +215,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz",
|
||||
"integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==",
|
||||
"version": "7.29.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.13.tgz",
|
||||
"integrity": "sha512-v+oN26xP4YIlQsbQ/CbTH1WG5m/l/DjKKZNy/Kp3f/AFvbgt7Em/D0FeMM3O2hX0YzeN2M16W8lOpyhIkfCBcQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -267,9 +267,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/regexpp": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.3.tgz",
|
||||
"integrity": "sha512-R76UKbvsqI68DJJVTVyqFKqU98CN4+B+acxNfEA2IFarSzP0SjtKf/3+fFiXzTyv5Fj3PxLkGHe2wnVWWMxobQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -843,9 +843,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.1.0.tgz",
|
||||
"integrity": "sha512-K+NU2HDCEem4sClgHH4ZZXwdJeNFkJo+hRk479DIwdIviAPN2hFJH1KHHwg8CxHQXvi0M5XY4deTXINfIiSG6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
@@ -2270,9 +2270,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.13.16",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.16.tgz",
|
||||
"integrity": "sha512-AOLOEhvrV+iH92xXCnFr/5+3KjPKcI3xP63XJtX5spAnkznPdHijlIsUTpi7/9EhWqrTCekw5c5zh1K+YsVw7A==",
|
||||
"version": "4.13.17",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.17.tgz",
|
||||
"integrity": "sha512-vZIcbkvjOR5hHRWERbGkr8fGfF0GGM1ooYkuH448iO3b2AdVEr3AXZSw4Z3Kn+1L5z0CVoVeyV29VGYmFh05kQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -2408,9 +2408,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.15",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.15.tgz",
|
||||
"integrity": "sha512-qdF6ewcZARtef63fJ9AKKZPgxw+t7USH6HLXYcD4CrdmSvDp79rpJSPFEKc90QBeGS63u1KQtN2MZQHC0yDIzA==",
|
||||
"version": "6.2.16",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.16.tgz",
|
||||
"integrity": "sha512-4LSJpbuBuPdGrONCdZUy0xnxtLf8eEz+EY0SMXdHQEBU7bgUHDVblU/JAjryv3wID9/LK7/YuwFwq+AQNfMscQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
@@ -3699,17 +3699,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
|
||||
"integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",
|
||||
"version": "8.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz",
|
||||
"integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.33.0",
|
||||
"picomatch": "^4.0.7",
|
||||
"postcss": "^8.5.30",
|
||||
"rolldown": "~1.2.14",
|
||||
"postcss": "^8.5.32",
|
||||
"rolldown": "~1.2.16",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -69,12 +69,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "2.4.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.318",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.319",
|
||||
"diff": "9.0.0",
|
||||
"zod": "4.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "1.4.0",
|
||||
"@anthropic-ai/sdk": "1.6.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tsconfig/node22": "22.0.6",
|
||||
"@types/node": "26.6.6",
|
||||
|
||||
204
src/acp-agent.ts
204
src/acp-agent.ts
@@ -1,13 +1,11 @@
|
||||
import {
|
||||
agent as acpAgent,
|
||||
AgentApp,
|
||||
AgentContext,
|
||||
AuthenticateRequest,
|
||||
AuthMethod,
|
||||
AvailableCommand,
|
||||
CancelNotification,
|
||||
ClientCapabilities,
|
||||
CompleteElicitationNotification,
|
||||
CreateElicitationRequest,
|
||||
CreateElicitationResponse,
|
||||
DisableProviderRequest,
|
||||
@@ -37,7 +35,6 @@ import {
|
||||
RequestError,
|
||||
SetProviderRequest,
|
||||
SetProviderResponse,
|
||||
RequestPermissionRequest,
|
||||
RequestPermissionResponse,
|
||||
ResumeSessionRequest,
|
||||
ResumeSessionResponse,
|
||||
@@ -106,6 +103,10 @@ import {
|
||||
toGoalSnapshot,
|
||||
} from "./goal-extension.js";
|
||||
import { blobResourceToClaude } from "./blob-resources.js";
|
||||
import { ChangedMetaClient, ClientConnection } from "./acp-client.js";
|
||||
import type { AcpClient, Logger } from "./acp-client.js";
|
||||
|
||||
export type { AcpClient, Logger } from "./acp-client.js";
|
||||
import { promptSuggestionsMeta, promptSuggestionText } from "./prompt-suggestions.js";
|
||||
import { sanitizeTitle, SessionTitles } from "./session-titles.js";
|
||||
import {
|
||||
@@ -284,6 +285,7 @@ import {
|
||||
import { IdleCompaction } from "./idle-compaction.js";
|
||||
import {
|
||||
clientSupportsNotices,
|
||||
collapseHookSpeaker,
|
||||
MAX_NOTICE_TITLE_LENGTH,
|
||||
normalizeNoticeText,
|
||||
noticeOrTranscriptUpdate,
|
||||
@@ -317,7 +319,6 @@ import {
|
||||
withRunningTerminal,
|
||||
withRunningToolCall,
|
||||
} from "./tool-calls/background.js";
|
||||
import { ChangedMetaFilter } from "./tool-calls/changed-meta-filter.js";
|
||||
import { ToolCallFieldTracker } from "./tool-calls/field-tracker.js";
|
||||
import { ClientCapabilities as ToolCallClientCapabilities } from "./tool-calls/client-capabilities.js";
|
||||
import {
|
||||
@@ -426,16 +427,6 @@ function isEmptyUserInterruptionDiagnostic(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger interface for customizing logging output
|
||||
*/
|
||||
export interface Logger {
|
||||
log: (...args: any[]) => void;
|
||||
error: (...args: any[]) => void;
|
||||
/** Optional: a caller that supplies no `warn` gets its warnings on `error`. */
|
||||
warn?: (...args: any[]) => void;
|
||||
}
|
||||
|
||||
type AccumulatedUsage = {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
@@ -2408,137 +2399,6 @@ export function isReplayHiddenMetaMessage(message: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-facing surface the agent calls back into. This is the subset of ACP
|
||||
* client methods the agent actually uses, expressed as a narrow interface so
|
||||
* tests can supply lightweight mocks. In production it is backed by
|
||||
* {@link ClientConnection} over the SDK's typed `AgentContext`.
|
||||
*/
|
||||
export interface AcpClient {
|
||||
sessionUpdate(params: AcpSessionNotification): Promise<void>;
|
||||
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
|
||||
* permission request so the client can dismiss its prompt, and rejects the
|
||||
* call at once: a client that ignores the cancel never answers. */
|
||||
requestPermission(
|
||||
params: AcpPermissionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestPermissionResponse>;
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse>;
|
||||
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse>;
|
||||
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
|
||||
* elicitation so the client can dismiss its prompt, and rejects the call
|
||||
* at once, as {@link requestPermission} does. */
|
||||
createElicitation(
|
||||
params: CreateElicitationRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CreateElicitationResponse>;
|
||||
completeElicitation(params: CompleteElicitationNotification): Promise<void>;
|
||||
/** Send a custom (extension) notification, e.g. `_claude/sdkMessage`. */
|
||||
extNotification(method: string, params: Record<string, unknown>): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridges {@link AcpClient} to the connection-scoped {@link AgentContext}
|
||||
* exposed by `AgentApp.connect(...)` as `connection.client`. The peer handle is
|
||||
* valid for the entire connection lifetime, so it is captured once at
|
||||
* construction.
|
||||
*/
|
||||
class ClientConnection implements AcpClient {
|
||||
constructor(private readonly ctx: AgentContext) {}
|
||||
|
||||
sessionUpdate(params: AcpSessionNotification): Promise<void> {
|
||||
return this.ctx.notify(methods.client.session.update, asSdkSessionNotification(params));
|
||||
}
|
||||
|
||||
requestPermission(
|
||||
params: RequestPermissionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
return this.ctx.request(methods.client.session.requestPermission, params, {
|
||||
cancellationSignal: signal,
|
||||
stopWaitingOnCancel: true,
|
||||
});
|
||||
}
|
||||
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
||||
return this.ctx.request(methods.client.fs.readTextFile, params);
|
||||
}
|
||||
|
||||
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
||||
return this.ctx.request(methods.client.fs.writeTextFile, params);
|
||||
}
|
||||
|
||||
createElicitation(
|
||||
params: CreateElicitationRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CreateElicitationResponse> {
|
||||
return this.ctx.request(methods.client.elicitation.create, params, {
|
||||
cancellationSignal: signal,
|
||||
stopWaitingOnCancel: true,
|
||||
});
|
||||
}
|
||||
|
||||
completeElicitation(params: CompleteElicitationNotification): Promise<void> {
|
||||
return this.ctx.notify(methods.client.elicitation.complete, params);
|
||||
}
|
||||
|
||||
extNotification(method: string, params: Record<string, unknown>): Promise<void> {
|
||||
return this.ctx.notify(method, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The client that the agent talks to. For an AIR client, every session update
|
||||
* passes the {@link ChangedMetaFilter} last, after the native subagent routing,
|
||||
* so a `tool_call_update` carries only the `_meta` keys that changed. ACP
|
||||
* merges only the top-level tool call fields, not the `_meta` keys. So another
|
||||
* client gets the full `_meta` on each update.
|
||||
*/
|
||||
class ChangedMetaClient implements AcpClient {
|
||||
private readonly filter = new ChangedMetaFilter();
|
||||
|
||||
constructor(
|
||||
private readonly inner: AcpClient,
|
||||
private readonly airClient: () => boolean,
|
||||
) {}
|
||||
|
||||
async sessionUpdate(params: AcpSessionNotification): Promise<void> {
|
||||
if (!this.airClient()) return this.inner.sessionUpdate(params);
|
||||
const update = this.filter.apply(params.update as SessionNotification["update"]);
|
||||
if (update) await this.inner.sessionUpdate({ ...params, update } as AcpSessionNotification);
|
||||
}
|
||||
|
||||
requestPermission(
|
||||
params: AcpPermissionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
return this.inner.requestPermission(params, signal);
|
||||
}
|
||||
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
||||
return this.inner.readTextFile(params);
|
||||
}
|
||||
|
||||
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
||||
return this.inner.writeTextFile(params);
|
||||
}
|
||||
|
||||
createElicitation(
|
||||
params: CreateElicitationRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CreateElicitationResponse> {
|
||||
return this.inner.createElicitation(params, signal);
|
||||
}
|
||||
|
||||
completeElicitation(params: CompleteElicitationNotification): Promise<void> {
|
||||
return this.inner.completeElicitation(params);
|
||||
}
|
||||
|
||||
extNotification(method: string, params: Record<string, unknown>): Promise<void> {
|
||||
return this.inner.extNotification(method, params);
|
||||
}
|
||||
}
|
||||
|
||||
function raceWithAbort<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
||||
@@ -4660,6 +4520,8 @@ export class ClaudeAcpAgent {
|
||||
let lastAssistantTotalUsage: number | null = null;
|
||||
let lastAssistantUsage: UsageSnapshot | null = null;
|
||||
let lastAssistantModel: string | null = null;
|
||||
// A rate_limit_event that came before the turn had any usage to report.
|
||||
let pendingRateLimit: unknown = null;
|
||||
// When the Claude SDK classifies a turn as failed (e.g. rate limit, auth
|
||||
// problem, billing), it sets a categorical `error` field on the
|
||||
// `SDKAssistantMessage` that precedes the final `result` message. We capture
|
||||
@@ -4967,7 +4829,8 @@ export class ClaudeAcpAgent {
|
||||
};
|
||||
|
||||
// ACP's usage_update has no model field, so the model the usage belongs
|
||||
// to rides in `_meta` alongside the other `_claude/*` keys.
|
||||
// to rides in `_meta` alongside the other `_claude/*` keys, as does a
|
||||
// pending rate limit.
|
||||
const attachUsageModel = <
|
||||
T extends {
|
||||
sessionUpdate: "usage_update";
|
||||
@@ -4978,6 +4841,13 @@ export class ClaudeAcpAgent {
|
||||
>(
|
||||
update: T,
|
||||
): T => {
|
||||
if (pendingRateLimit !== null) {
|
||||
update = {
|
||||
...update,
|
||||
_meta: { "_claude/rateLimit": pendingRateLimit, ...(update._meta ?? {}) },
|
||||
};
|
||||
pendingRateLimit = null;
|
||||
}
|
||||
if (!lastAssistantModel) return update;
|
||||
return {
|
||||
...update,
|
||||
@@ -6404,8 +6274,6 @@ export class ClaudeAcpAgent {
|
||||
!firstUnsettledQueuedTurn()
|
||||
) {
|
||||
await asyncTasks.autonomousTurnStarted();
|
||||
} else if (message.state === "idle" && asyncTasks.autonomousTurnOpen) {
|
||||
await asyncTasks.autonomousTurnEnded("completed");
|
||||
}
|
||||
if (
|
||||
message.state === "running" &&
|
||||
@@ -6437,6 +6305,9 @@ export class ClaudeAcpAgent {
|
||||
// here. An owed idle belongs to an earlier result: it can lag
|
||||
// past the cycle's first output.
|
||||
if (session.owedTrailingIdles === 0) {
|
||||
if (asyncTasks.autonomousTurnOpen) {
|
||||
await asyncTasks.autonomousTurnEnded("completed");
|
||||
}
|
||||
await closeCycleToolCalls();
|
||||
this.endAutonomousCycle(params.sessionId, {
|
||||
outcome: { stopReason: session.cancelled ? "cancelled" : "end_turn" },
|
||||
@@ -6709,10 +6580,11 @@ export class ClaudeAcpAgent {
|
||||
}
|
||||
}
|
||||
const severity = message.level === "warning" ? "warning" : "info";
|
||||
const transcriptContent = collapseHookSpeaker(message.content);
|
||||
const transcriptText =
|
||||
message.level === "info"
|
||||
? message.content
|
||||
: `**${sentenceCase(message.level)}:** ${message.content}`;
|
||||
? transcriptContent
|
||||
: `**${sentenceCase(message.level)}:** ${transcriptContent}`;
|
||||
await sendUpdate({
|
||||
sessionId: params.sessionId,
|
||||
update: noticeOrTranscriptUpdate(
|
||||
@@ -8502,6 +8374,18 @@ export class ClaudeAcpAgent {
|
||||
content,
|
||||
toolUseResult,
|
||||
session.toolUseCache,
|
||||
(toolUseId) => {
|
||||
for (const [taskId, task] of session.liveBackgroundTasks) {
|
||||
if (
|
||||
task.parentToolUseId === toolUseId &&
|
||||
task.taskType === "local_bash" &&
|
||||
task.backgrounded
|
||||
) {
|
||||
return taskId;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
if (backgroundBashTask) await asyncTasks.taskBackgrounded(backgroundBashTask);
|
||||
// The command's terminal runs until its task ends, unless the
|
||||
@@ -8676,7 +8560,11 @@ export class ClaudeAcpAgent {
|
||||
}
|
||||
case "rate_limit_event": {
|
||||
session.idleCompaction?.noteRateLimit(message.rate_limit_info);
|
||||
if (lastAssistantTotalUsage !== null) {
|
||||
// With partial messages on, the SDK sends this before the turn's
|
||||
// first assistant frame, while there is no usage yet to send it with.
|
||||
if (lastAssistantTotalUsage === null) {
|
||||
pendingRateLimit = message.rate_limit_info;
|
||||
} else {
|
||||
await sendUpdate({
|
||||
sessionId: params.sessionId,
|
||||
update: attachUsageModel({
|
||||
@@ -10025,9 +9913,10 @@ export class ClaudeAcpAgent {
|
||||
// The entity is keyed by the summary message's uuid, which differs from
|
||||
// the live id (the `compacting` status uuid) — replay is terminal-first
|
||||
// by the RFD, and no client merges a replay into a live entity store.
|
||||
// A subagent's own compaction summary is not the root session's, and a
|
||||
// summary whose framing is unrecognized stays visible as transcript
|
||||
// text rather than vanishing behind a summary-less entity.
|
||||
// A subagent's own compaction summary is not the root session's. A summary
|
||||
// whose framing is unrecognized is still this compaction, without summary
|
||||
// text: isCompactSummary says what it is, and the framing is Claude Code's
|
||||
// to change.
|
||||
if (
|
||||
replayCompactionUpdates &&
|
||||
isCompactSummaryMessage(message) &&
|
||||
@@ -10043,10 +9932,9 @@ export class ClaudeAcpAgent {
|
||||
airClient: this.toolCallCapabilities.air.client,
|
||||
},
|
||||
);
|
||||
if (replayCompaction.recordSummary(assistantMessageText(message.message))) {
|
||||
await replayCompaction.finish(message.uuid, "completed");
|
||||
return;
|
||||
}
|
||||
replayCompaction.recordSummary(assistantMessageText(message.message));
|
||||
await replayCompaction.finish(message.uuid, "completed");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const notification of toAcpNotifications(
|
||||
|
||||
158
src/acp-client.ts
Normal file
158
src/acp-client.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import {
|
||||
AgentContext,
|
||||
CompleteElicitationNotification,
|
||||
CreateElicitationRequest,
|
||||
CreateElicitationResponse,
|
||||
methods,
|
||||
ReadTextFileRequest,
|
||||
ReadTextFileResponse,
|
||||
RequestPermissionRequest,
|
||||
RequestPermissionResponse,
|
||||
SessionNotification,
|
||||
WriteTextFileRequest,
|
||||
WriteTextFileResponse,
|
||||
} from "@agentclientprotocol/sdk/unstable";
|
||||
import { AcpSessionNotification, asSdkSessionNotification } from "./acp-subagents.js";
|
||||
import { type AcpPermissionRequest } from "./permissions/presentation.js";
|
||||
import { ChangedMetaFilter } from "./tool-calls/changed-meta-filter.js";
|
||||
|
||||
/**
|
||||
* Logger interface for customizing logging output
|
||||
*/
|
||||
export interface Logger {
|
||||
log: (...args: any[]) => void;
|
||||
error: (...args: any[]) => void;
|
||||
/** Optional: a caller that supplies no `warn` gets its warnings on `error`. */
|
||||
warn?: (...args: any[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-facing surface the agent calls back into. This is the subset of ACP
|
||||
* client methods the agent actually uses, expressed as a narrow interface so
|
||||
* tests can supply lightweight mocks. In production it is backed by
|
||||
* {@link ClientConnection} over the SDK's typed `AgentContext`.
|
||||
*/
|
||||
export interface AcpClient {
|
||||
sessionUpdate(params: AcpSessionNotification): Promise<void>;
|
||||
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
|
||||
* permission request so the client can dismiss its prompt, and rejects the
|
||||
* call at once: a client that ignores the cancel never answers. */
|
||||
requestPermission(
|
||||
params: AcpPermissionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestPermissionResponse>;
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse>;
|
||||
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse>;
|
||||
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
|
||||
* elicitation so the client can dismiss its prompt, and rejects the call
|
||||
* at once, as {@link requestPermission} does. */
|
||||
createElicitation(
|
||||
params: CreateElicitationRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CreateElicitationResponse>;
|
||||
completeElicitation(params: CompleteElicitationNotification): Promise<void>;
|
||||
/** Send a custom (extension) notification, e.g. `_claude/sdkMessage`. */
|
||||
extNotification(method: string, params: Record<string, unknown>): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridges {@link AcpClient} to the connection-scoped {@link AgentContext}
|
||||
* exposed by `AgentApp.connect(...)` as `connection.client`. The peer handle is
|
||||
* valid for the entire connection lifetime, so it is captured once at
|
||||
* construction.
|
||||
*/
|
||||
export class ClientConnection implements AcpClient {
|
||||
constructor(private readonly ctx: AgentContext) {}
|
||||
|
||||
sessionUpdate(params: AcpSessionNotification): Promise<void> {
|
||||
return this.ctx.notify(methods.client.session.update, asSdkSessionNotification(params));
|
||||
}
|
||||
|
||||
requestPermission(
|
||||
params: RequestPermissionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
return this.ctx.request(methods.client.session.requestPermission, params, {
|
||||
cancellationSignal: signal,
|
||||
stopWaitingOnCancel: true,
|
||||
});
|
||||
}
|
||||
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
||||
return this.ctx.request(methods.client.fs.readTextFile, params);
|
||||
}
|
||||
|
||||
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
||||
return this.ctx.request(methods.client.fs.writeTextFile, params);
|
||||
}
|
||||
|
||||
createElicitation(
|
||||
params: CreateElicitationRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CreateElicitationResponse> {
|
||||
return this.ctx.request(methods.client.elicitation.create, params, {
|
||||
cancellationSignal: signal,
|
||||
stopWaitingOnCancel: true,
|
||||
});
|
||||
}
|
||||
|
||||
completeElicitation(params: CompleteElicitationNotification): Promise<void> {
|
||||
return this.ctx.notify(methods.client.elicitation.complete, params);
|
||||
}
|
||||
|
||||
extNotification(method: string, params: Record<string, unknown>): Promise<void> {
|
||||
return this.ctx.notify(method, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The client that the agent talks to. For an AIR client, every session update
|
||||
* passes the {@link ChangedMetaFilter} last, after the native subagent routing,
|
||||
* so a `tool_call_update` carries only the `_meta` keys that changed. ACP
|
||||
* merges only the top-level tool call fields, not the `_meta` keys. So another
|
||||
* client gets the full `_meta` on each update.
|
||||
*/
|
||||
export class ChangedMetaClient implements AcpClient {
|
||||
private readonly filter = new ChangedMetaFilter();
|
||||
|
||||
constructor(
|
||||
private readonly inner: AcpClient,
|
||||
private readonly airClient: () => boolean,
|
||||
) {}
|
||||
|
||||
async sessionUpdate(params: AcpSessionNotification): Promise<void> {
|
||||
if (!this.airClient()) return this.inner.sessionUpdate(params);
|
||||
const update = this.filter.apply(params.update as SessionNotification["update"]);
|
||||
if (update) await this.inner.sessionUpdate({ ...params, update } as AcpSessionNotification);
|
||||
}
|
||||
|
||||
requestPermission(
|
||||
params: AcpPermissionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
return this.inner.requestPermission(params, signal);
|
||||
}
|
||||
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
||||
return this.inner.readTextFile(params);
|
||||
}
|
||||
|
||||
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
||||
return this.inner.writeTextFile(params);
|
||||
}
|
||||
|
||||
createElicitation(
|
||||
params: CreateElicitationRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CreateElicitationResponse> {
|
||||
return this.inner.createElicitation(params, signal);
|
||||
}
|
||||
|
||||
completeElicitation(params: CompleteElicitationNotification): Promise<void> {
|
||||
return this.inner.completeElicitation(params);
|
||||
}
|
||||
|
||||
extNotification(method: string, params: Record<string, unknown>): Promise<void> {
|
||||
return this.inner.extNotification(method, params);
|
||||
}
|
||||
}
|
||||
@@ -863,18 +863,29 @@ function recordPublished(task: AsyncTask, fields: Record<string, unknown>): void
|
||||
* Recovers a background Bash task from the structured `backgroundTaskId` of
|
||||
* its tool result. The text of a tool result never gives a task id: a
|
||||
* foreground command can print any text.
|
||||
*
|
||||
* Claude Code sends no `tool_use_result` with a subagent's tool results. For
|
||||
* those, `startedTaskOf` gives the background `local_bash` task whose
|
||||
* `task_started` named the Bash call, which comes before the result.
|
||||
*/
|
||||
export function backgroundBashTaskFromToolResult(
|
||||
content: unknown,
|
||||
toolUseResult: unknown,
|
||||
toolUses: Record<string, { name: string; input: unknown }>,
|
||||
startedTaskOf?: (toolUseId: string) => string | undefined,
|
||||
): (AsyncTaskStarted & { taskId: string; toolCallId: string }) | undefined {
|
||||
if (!Array.isArray(content)) return undefined;
|
||||
const toolResults = content.flatMap(toolResultBlock);
|
||||
const bashResults = toolResults.filter((result) => toolUses[result.toolUseId]?.name === "Bash");
|
||||
if (bashResults.length === 0) return undefined;
|
||||
|
||||
const backgroundResults = structuredBackgroundResults(toolUseResult, toolUses);
|
||||
let backgroundResults = structuredBackgroundResults(toolUseResult, toolUses);
|
||||
if (backgroundResults.length === 0 && startedTaskOf) {
|
||||
backgroundResults = bashResults.flatMap((result) => {
|
||||
const taskId = result.isError ? undefined : startedTaskOf(result.toolUseId);
|
||||
return taskId ? [{ taskId, toolUseId: result.toolUseId }] : [];
|
||||
});
|
||||
}
|
||||
const distinctTaskIds = new Set(backgroundResults.map((result) => result.taskId));
|
||||
if (distinctTaskIds.size !== 1) return undefined;
|
||||
const taskId = [...distinctTaskIds][0];
|
||||
|
||||
@@ -210,6 +210,15 @@ export function fromCliStatus(stdout: string, apiKeyHelper?: string): AuthStatus
|
||||
if (!externalBackend && !status.loggedIn && !status.apiKeySource) {
|
||||
return notLoggedInAuthStatus();
|
||||
}
|
||||
if (
|
||||
status.loggedIn &&
|
||||
status.authMethod === "oauth_token" &&
|
||||
status.apiProvider === "firstParty" &&
|
||||
!status.apiKeySource &&
|
||||
!status.subscriptionType
|
||||
) {
|
||||
return { kind: "account", label: "Claude subscription", detail: "OAuth token" };
|
||||
}
|
||||
return mapAuthFields({
|
||||
apiProvider: status.apiProvider,
|
||||
subscriptionType: status.subscriptionType,
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
* "Background work").
|
||||
*
|
||||
* The lists are built from Claude Code's live background task set
|
||||
* (`background_tasks_changed`), without the tasks it marks `ambient`, which
|
||||
* are housekeeping that it started on its own. The root session and each
|
||||
* (`background_tasks_changed`). Claude Code marks `ambient` its own
|
||||
* housekeeping and every live-update watcher; those are left out, except a
|
||||
* task that a tool call started, such as a `Monitor` call's. The root session and each
|
||||
* announced subagent session (the subagents RFD form) has its own list. Each
|
||||
* task is listed once, in the list of the session that runs it:
|
||||
*
|
||||
@@ -166,7 +167,11 @@ export class SessionBackgroundWork {
|
||||
* `Monitor` or `Workflow` call, whose structured result names its task,
|
||||
* or a call that a live task names as its tool use, such as a
|
||||
* backgrounded MCP call. The task must be in the level: one that ended
|
||||
* already, or that Claude Code marks `ambient`, leaves its call as it is.
|
||||
* already leaves its call as it is. Claude Code marks every live-update
|
||||
* watcher `ambient`, including the task of a `Monitor` call; a call
|
||||
* started it, so it is work the client can see and stop, and is
|
||||
* registered all the same. Ambient tasks without a call of their own,
|
||||
* such as watchers Claude Code starts by itself, are never registered.
|
||||
*
|
||||
* Returns the ids of the registered calls, whose result then reports
|
||||
* `in_progress` (see `withRunningToolCall`).
|
||||
@@ -210,12 +215,7 @@ export class SessionBackgroundWork {
|
||||
([, task]) => task.parentToolUseId === toolCallId && !task.isSubagent,
|
||||
)?.[0];
|
||||
const task = taskId === undefined ? undefined : this.level.get(taskId);
|
||||
if (
|
||||
!task ||
|
||||
task.ambient ||
|
||||
task.taskType === "local_bash" ||
|
||||
task.taskType === "local_agent"
|
||||
) {
|
||||
if (!task || task.taskType === "local_bash" || task.taskType === "local_agent") {
|
||||
continue;
|
||||
}
|
||||
this.toolCalls.set(taskId!, {
|
||||
@@ -363,6 +363,15 @@ export class SessionBackgroundWork {
|
||||
const ownerOf = (parentToolUseId: string | undefined) =>
|
||||
parentToolUseId ? childByToolUse.get(parentToolUseId)?.sessionId : this.sessionId;
|
||||
for (const [taskId, task] of this.level) {
|
||||
// A registered call's task is listed even when ambient, as a `Monitor`
|
||||
// call's is (see `toolResults`).
|
||||
const toolCall = this.toolCalls.get(taskId);
|
||||
if (toolCall) {
|
||||
const session = ownerOf(toolCall.parentToolUseId);
|
||||
if (session)
|
||||
lists.get(session)!.push({ type: "tool_call", toolCallId: toolCall.toolCallId });
|
||||
continue;
|
||||
}
|
||||
if (task.ambient) continue;
|
||||
const terminal = this.terminals.get(taskId);
|
||||
if (terminal) {
|
||||
@@ -371,13 +380,6 @@ export class SessionBackgroundWork {
|
||||
lists.get(session)!.push({ type: "terminal", terminalId: terminal.toolCallId });
|
||||
continue;
|
||||
}
|
||||
const toolCall = this.toolCalls.get(taskId);
|
||||
if (toolCall) {
|
||||
const session = ownerOf(toolCall.parentToolUseId);
|
||||
if (session)
|
||||
lists.get(session)!.push({ type: "tool_call", toolCallId: toolCall.toolCallId });
|
||||
continue;
|
||||
}
|
||||
// A command without a terminal yet is one that foreground work waits on.
|
||||
// A subagent is listed below, as its session.
|
||||
if (task.taskType === "local_bash" || task.taskType === "local_agent") continue;
|
||||
|
||||
@@ -67,7 +67,7 @@ const ANALYSIS_BLOCKS = [
|
||||
* it as the post-compaction user message. */
|
||||
const CONTINUATION_PREAMBLE = /^\s*This session is being continued from a previous conversation/i;
|
||||
const PERSISTED_PREAMBLE =
|
||||
/^\s*This session is being continued from a previous conversation[^\n]*\n+Summary:[ \t]*\r?\n/;
|
||||
/^\s*This session is being continued from a previous conversation[^\n]*\n(?:[\s\S]*?\n)?Summary:[ \t]*\r?\n/;
|
||||
const PERSISTED_TRAILERS = [
|
||||
/\n+If you need specific details from before compaction[\s\S]*$/,
|
||||
/\n+(?:Please )?[Cc]ontinue the conversation from where it left (?:it )?off[\s\S]*$/,
|
||||
|
||||
@@ -303,7 +303,7 @@ export function applyAskElicitationResponse(
|
||||
questions: AskUserQuestion[],
|
||||
options: { reportUnanswered?: boolean } = {},
|
||||
): AskUserQuestionOutcome {
|
||||
// Typed against the output schema: as of SDK 0.3.318 only it lists
|
||||
// Typed against the output schema: as of SDK 0.3.319 only it lists
|
||||
// `unanswered`, though the tool reads the same annotations from its input.
|
||||
const annotations: NonNullable<AskUserQuestionOutput["annotations"]> = {};
|
||||
const withAnnotations = (answers: AskUserQuestionOutput["answers"]) => ({
|
||||
|
||||
@@ -50,11 +50,35 @@ export function noticeOrTranscriptUpdate(
|
||||
if (supportsNotices) return noticeUpdate(notice);
|
||||
return {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: transcriptText },
|
||||
content: { type: "text", text: transcriptBlock(transcriptText) },
|
||||
...(transcriptMeta ? { _meta: transcriptMeta } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Clients render consecutive agent_message_chunks as one markdown stream, so
|
||||
* a bare `**Notice:** …` chunk lands mid-sentence: "…all done.**Notice:** Stop
|
||||
* says: …". Put it in its own blockquote paragraph, blank lines on both sides,
|
||||
* so it starts on a new line and Claude's next chunk does not continue the
|
||||
* quote. */
|
||||
export function transcriptBlock(text: string): string {
|
||||
const lines = text.trim().split(/\r?\n/);
|
||||
return `\n\n${lines.map((line) => (line ? `> ${line}` : ">")).join("\n")}\n\n`;
|
||||
}
|
||||
|
||||
const HOOK_SPEAKER = /^[^\n:]{1,80} says: /;
|
||||
|
||||
/** Claude Code prefixes every line of a hook's `systemMessage` with
|
||||
* "<HookEvent> says: ", so a two-line message reads "Stop says: a\nStop says:
|
||||
* b". Keep the prefix on the first line only when every line repeats it. */
|
||||
export function collapseHookSpeaker(text: string): string {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const prefix = lines[0].match(HOOK_SPEAKER)?.[0];
|
||||
if (!prefix || lines.length < 2 || !lines.every((line) => line.startsWith(prefix))) {
|
||||
return text;
|
||||
}
|
||||
return [lines[0], ...lines.slice(1).map((line) => line.slice(prefix.length))].join("\n");
|
||||
}
|
||||
|
||||
/** Fragments that read on after a bold label stand alone as a notice
|
||||
* description, so start them with a capital. */
|
||||
export function sentenceCase(text: string): string {
|
||||
|
||||
@@ -25,7 +25,7 @@ export type UltracodeSettingsReport = {
|
||||
};
|
||||
|
||||
// `Query.getSettings()` exists at runtime (sdk.mjs) but is missing from the
|
||||
// SDK's type declarations as of 0.3.318.
|
||||
// SDK's type declarations as of 0.3.319.
|
||||
type GetSettingsQuery = Query & {
|
||||
getSettings?: () => Promise<{
|
||||
applied?: { ultracodeAvailable?: unknown; ultracodeRequested?: unknown };
|
||||
|
||||
@@ -13858,7 +13858,7 @@ describe("MCP server startup failures", () => {
|
||||
);
|
||||
expect(lines).toHaveLength(1);
|
||||
expect(lines[0].content.text).toBe(
|
||||
'**MCP server "client-fs" failed to start:** Its tools are unavailable in this session. Run /mcp for the error.',
|
||||
'\n\n> **MCP server "client-fs" failed to start:** Its tools are unavailable in this session. Run /mcp for the error.\n\n',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -18610,8 +18610,8 @@ describe("assembled assistant text fallback", () => {
|
||||
]);
|
||||
expect(child.some((update) => update.sessionUpdate === "compaction_update")).toBe(false);
|
||||
|
||||
// Unrecognized framing: the text stays visible rather than vanishing
|
||||
// behind a summary-less entity.
|
||||
// Unrecognized framing: still the compaction isCompactSummary says it is,
|
||||
// without summary text, and never a message from the user.
|
||||
const unknownFraming = await replay(true, [
|
||||
{
|
||||
...summaryMessage("odd-summary"),
|
||||
@@ -18622,12 +18622,14 @@ describe("assembled assistant text fallback", () => {
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(unknownFraming.some((update) => update.sessionUpdate === "compaction_update")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(unknownFraming.some((update) => update.sessionUpdate === "user_message_chunk")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(unknownFraming).toEqual([
|
||||
{
|
||||
sessionUpdate: "compaction_update",
|
||||
compactionId: "odd-summary",
|
||||
status: "completed",
|
||||
_meta: { jetbrains: { air: { version: 1, contextCompaction: { version: 1 } } } },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves the model response after multiple compactions in one turn", async () => {
|
||||
@@ -18848,7 +18850,7 @@ describe("assembled assistant text fallback", () => {
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "1+2" }] });
|
||||
|
||||
const chunks = updates.filter(({ update }) => update.sessionUpdate === "agent_message_chunk");
|
||||
expect(messageChunkTexts(updates)).toEqual(["**Warning:** hook says no"]);
|
||||
expect(messageChunkTexts(updates)).toEqual(["\n\n> **Warning:** hook says no\n\n"]);
|
||||
expect(chunks[0]?.update).toMatchObject({
|
||||
_meta: { claudeCode: { kind: "informational", level: "warning" } },
|
||||
});
|
||||
@@ -18903,12 +18905,33 @@ describe("assembled assistant text fallback", () => {
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "1+2" }] });
|
||||
|
||||
const chunks = updates.filter(({ update }) => update.sessionUpdate === "agent_message_chunk");
|
||||
expect(messageChunkTexts(updates)).toEqual(["plain hook detail"]);
|
||||
expect(messageChunkTexts(updates)).toEqual(["\n\n> plain hook detail\n\n"]);
|
||||
expect(chunks[0]?.update).toMatchObject({
|
||||
_meta: { claudeCode: { kind: "informational", level: "info" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a multi-line hook notice as one quote with a single speaker prefix", async () => {
|
||||
const { agent, updates } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
{
|
||||
type: "system",
|
||||
subtype: "informational",
|
||||
content: "Stop says: token: 3 calls\nStop says: hand off now",
|
||||
level: "notice",
|
||||
session_id: "test-session",
|
||||
},
|
||||
result(),
|
||||
idle,
|
||||
]);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "1+2" }] });
|
||||
|
||||
expect(messageChunkTexts(updates)).toEqual([
|
||||
"\n\n> **Notice:** Stop says: token: 3 calls\n> hand off now\n\n",
|
||||
]);
|
||||
});
|
||||
|
||||
it("delivers an informational frame as a notice and still owns the result text", async () => {
|
||||
const { agent, updates } = createMockAgentWithCapture();
|
||||
await initializeClient(agent, { session: { notices: {} } });
|
||||
@@ -25951,6 +25974,82 @@ describe("deferred settlement for live background subagents (issues #864/#866)",
|
||||
]);
|
||||
});
|
||||
|
||||
// Issue #1453: a previous cycle's trailing idle completed the next turn task.
|
||||
it.each(["result", "idle"] as const)(
|
||||
"keeps the next turn task open across an owed idle until its own %s",
|
||||
async (end) => {
|
||||
const { agent, asyncTaskUpdates } = await airAgent();
|
||||
let wake = () => {};
|
||||
const woken = new Promise<void>((resolve) => (wake = resolve));
|
||||
let finish = () => {};
|
||||
const finished = new Promise<void>((resolve) => (finish = resolve));
|
||||
injectGeneratorSession(agent, (input) => {
|
||||
async function* messageGenerator() {
|
||||
const iter = input[Symbol.asyncIterator]();
|
||||
yield userEcho((await iter.next()).value);
|
||||
yield running();
|
||||
yield resultMessage();
|
||||
yield idle();
|
||||
await woken;
|
||||
yield running();
|
||||
yield assistantText("first autonomous cycle");
|
||||
yield resultMessage({ origin: { kind: "task-notification" } });
|
||||
// The CLI stays running: the next output opens a new turn task.
|
||||
yield assistantText("next autonomous cycle");
|
||||
yield idle(); // The previous autonomous result's late trailer.
|
||||
await finished;
|
||||
yield assistantText("still working in the next cycle");
|
||||
if (end === "result") {
|
||||
yield resultMessage({ origin: { kind: "task-notification" } });
|
||||
}
|
||||
yield idle();
|
||||
}
|
||||
return messageGenerator();
|
||||
});
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
wake();
|
||||
try {
|
||||
await waitFor(
|
||||
() =>
|
||||
asyncTaskUpdates().filter((u) => u.sessionUpdate === "async_task_spawned").length ===
|
||||
2 && agent.sessions["test-session"].owedTrailingIdles === 0,
|
||||
);
|
||||
const [first, firstEnd, next] = asyncTaskUpdates();
|
||||
expect(asyncTaskUpdates()).toEqual([
|
||||
first,
|
||||
{
|
||||
sessionUpdate: "async_task_state_update",
|
||||
asyncTaskId: first.asyncTaskId,
|
||||
state: "completed",
|
||||
},
|
||||
next,
|
||||
]);
|
||||
expect(firstEnd.state).toBe("completed");
|
||||
expect(next.sessionUpdate).toBe("async_task_spawned");
|
||||
expect(next.asyncTaskId).not.toBe(first.asyncTaskId);
|
||||
} finally {
|
||||
finish();
|
||||
await agent.sessions["test-session"]?.consumer;
|
||||
}
|
||||
const [first, , next] = asyncTaskUpdates();
|
||||
expect(asyncTaskUpdates()).toEqual([
|
||||
first,
|
||||
{
|
||||
sessionUpdate: "async_task_state_update",
|
||||
asyncTaskId: first.asyncTaskId,
|
||||
state: "completed",
|
||||
},
|
||||
next,
|
||||
{
|
||||
sessionUpdate: "async_task_state_update",
|
||||
asyncTaskId: next.asyncTaskId,
|
||||
state: "completed",
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
describe("started by a scheduler fire", () => {
|
||||
const lifecycleStarted = (fire: { wakeup_source?: string; scheduled_for?: number }) =>
|
||||
({
|
||||
@@ -30206,6 +30305,61 @@ describe("agent selection config option", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("rate_limit_event", () => {
|
||||
it("rides on the next usage_update when it comes before the turn's first assistant frame", async () => {
|
||||
const updates: SessionNotification[] = [];
|
||||
const agent = new ClaudeAcpAgent(
|
||||
{
|
||||
sessionUpdate: async (notification: SessionNotification) => {
|
||||
updates.push(notification);
|
||||
},
|
||||
} as unknown as AcpClient,
|
||||
{ log: () => {}, error: () => {} },
|
||||
);
|
||||
const rateLimit = { status: "allowed", resetsAt: 1700000000, rateLimitType: "seven_day" };
|
||||
const usage = {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
};
|
||||
injectGeneratorSession(agent, async function* (input) {
|
||||
const { value } = await input[Symbol.asyncIterator]().next();
|
||||
yield userEcho(value);
|
||||
yield {
|
||||
type: "rate_limit_event",
|
||||
rate_limit_info: rateLimit,
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
};
|
||||
yield {
|
||||
type: "assistant",
|
||||
parent_tool_use_id: null,
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
message: {
|
||||
id: "msg_rate",
|
||||
role: "assistant",
|
||||
model: "claude-sonnet-4-20250514",
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
usage,
|
||||
},
|
||||
};
|
||||
yield successfulResultMessage({ usage });
|
||||
yield { type: "system", subtype: "session_state_changed", state: "idle" };
|
||||
});
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
|
||||
const usageMetas = updates
|
||||
.map((notification) => notification.update)
|
||||
.filter((update) => update.sessionUpdate === "usage_update")
|
||||
.map((update) => update._meta?.["_claude/rateLimit"]);
|
||||
expect(usageMetas[0]).toEqual(rateLimit);
|
||||
expect(usageMetas.slice(1)).toEqual(usageMetas.slice(1).map(() => undefined));
|
||||
});
|
||||
});
|
||||
|
||||
describe("the SDK session id of a fresh Claude context", () => {
|
||||
// After a clear-context restart the SDK runs under a new session id, and
|
||||
// every SDK message carries it. The client knows only the ACP session id.
|
||||
|
||||
@@ -4510,10 +4510,13 @@ describe("ACP v2 requests that subagents make", () => {
|
||||
description: "make test",
|
||||
tool_use_id: "toolu_bg",
|
||||
parent_task_id: "agent-1",
|
||||
owned_by_subagent: true,
|
||||
is_backgrounded: true,
|
||||
uuid: randomUUID(),
|
||||
session_id: options.sessionId,
|
||||
};
|
||||
// As seen live (CLI 2.1.318): a subagent's tool result has no
|
||||
// `tool_use_result`, so only the `task_started` above names the task.
|
||||
yield childMessage(options, {
|
||||
type: "user",
|
||||
message: {
|
||||
@@ -4526,13 +4529,6 @@ describe("ACP v2 requests that subagents make", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
tool_use_result: {
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
interrupted: false,
|
||||
isImage: false,
|
||||
backgroundTaskId: "bg1",
|
||||
},
|
||||
});
|
||||
yield taskEnd(options, "agent-1", "toolu_agent-1");
|
||||
yield level(options, [bash]);
|
||||
@@ -5879,10 +5875,13 @@ describe("ACP v2 terminal of a command in the background", () => {
|
||||
});
|
||||
|
||||
describe("a task that a tool call started", () => {
|
||||
// Claude Code marks every live-update watcher ambient, a requested
|
||||
// Monitor's task included.
|
||||
const monitor = {
|
||||
task_id: "mon1",
|
||||
task_type: "local_monitor",
|
||||
description: "Watch the logs",
|
||||
ambient: true,
|
||||
};
|
||||
const workflow = { task_id: "wf1", task_type: "local_workflow", description: "Review" };
|
||||
/** A Monitor call whose task `mon1` runs on after the call returns. The
|
||||
@@ -6107,25 +6106,34 @@ describe("ACP v2 terminal of a command in the background", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lists a task without such a call as a _claude/task item", async () => {
|
||||
scriptTurns(async function* (options) {
|
||||
yield level(options, [monitor]);
|
||||
// No tool call started it, such as a task that a hook started.
|
||||
yield taskEvent(options, "task_started", monitor);
|
||||
yield result(options);
|
||||
yield idleState(options);
|
||||
});
|
||||
const client = await promptUntil("idle end_turn [_claude/task]");
|
||||
// No tool call started these, such as tasks that a hook started.
|
||||
it.each([
|
||||
{
|
||||
task: workflow,
|
||||
trace: ["running []", "running [_claude/task]", "idle end_turn [_claude/task]"],
|
||||
background: [{ type: "_claude/task", taskId: "wf1", description: "Review" }],
|
||||
},
|
||||
// A watcher Claude Code started by itself is housekeeping.
|
||||
{
|
||||
task: monitor,
|
||||
trace: ["running []", "idle end_turn []"],
|
||||
background: [],
|
||||
},
|
||||
])(
|
||||
"lists a $task.task_type task without such a call unless it is ambient",
|
||||
async ({ task, trace, background }) => {
|
||||
scriptTurns(async function* (options) {
|
||||
yield level(options, [task]);
|
||||
yield taskEvent(options, "task_started", task);
|
||||
yield result(options);
|
||||
yield idleState(options);
|
||||
});
|
||||
const client = await promptUntil(trace.at(-1)!);
|
||||
|
||||
expect(backgroundTrace(client)).toEqual([
|
||||
"running []",
|
||||
"running [_claude/task]",
|
||||
"idle end_turn [_claude/task]",
|
||||
]);
|
||||
expect(client.updates("state_update").at(-1)).toMatchObject({
|
||||
background: [{ type: "_claude/task", taskId: "mon1", description: "Watch the logs" }],
|
||||
});
|
||||
});
|
||||
expect(backgroundTrace(client)).toEqual(trace);
|
||||
expect(client.updates("state_update").at(-1)).toMatchObject({ background });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects terminal/cancel with -32602 when the command ends before Claude Code stops it", async () => {
|
||||
|
||||
@@ -168,7 +168,10 @@ describe("AsyncTaskRuntime", () => {
|
||||
expect(published[1]?.update).not.toHaveProperty("summary");
|
||||
expect(published.at(-1)?.update).toMatchObject({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "**Task stopped by user:** Build generated assets." },
|
||||
content: {
|
||||
type: "text",
|
||||
text: "\n\n> **Task stopped by user:** Build generated assets.\n\n",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -234,7 +237,7 @@ describe("AsyncTaskRuntime", () => {
|
||||
]);
|
||||
expect(published.at(-1)?.update).toMatchObject({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "**Task stopped by user:** npm run build." },
|
||||
content: { type: "text", text: "\n\n> **Task stopped by user:** npm run build.\n\n" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -264,7 +267,7 @@ describe("AsyncTaskRuntime", () => {
|
||||
});
|
||||
expect(published.at(-1)?.update).toMatchObject({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "**Task stopped by user:** npm run build." },
|
||||
content: { type: "text", text: "\n\n> **Task stopped by user:** npm run build.\n\n" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -306,6 +306,14 @@ describe("auth status mappers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("maps a setup-token CLI login without profile fields to an account", () => {
|
||||
expect(
|
||||
fromCliStatus(
|
||||
JSON.stringify({ loggedIn: true, authMethod: "oauth_token", apiProvider: "firstParty" }),
|
||||
),
|
||||
).toEqual({ kind: "account", label: "Claude subscription", detail: "OAuth token" });
|
||||
});
|
||||
|
||||
it("lets a third-party backend outrank the logged-out flag", () => {
|
||||
// `loggedIn` tracks the claude.ai login only; on Bedrock the credentials
|
||||
// are external and the agent is authenticated all the same.
|
||||
|
||||
@@ -130,6 +130,17 @@ describe("compactionSummaryText", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("strips the framing when other content sits before the Summary line", () => {
|
||||
expect(
|
||||
compactionSummaryText(
|
||||
"This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\n" +
|
||||
"<system-reminder>\n<tools-used-before-summary>Read, Bash</tools-used-before-summary>\n</system-reminder>\n" +
|
||||
"Summary:\n1. Primary Request and Intent:\n Count upward.\n\n" +
|
||||
"Continue the conversation from where it left off without asking the user any further questions.",
|
||||
),
|
||||
).toBe("1. Primary Request and Intent:\n Count upward.");
|
||||
});
|
||||
|
||||
it("strips the older tagged transcript framing", () => {
|
||||
expect(
|
||||
compactionSummaryText(
|
||||
|
||||
@@ -413,7 +413,7 @@ describe("syncFastModeState (SDK-driven state changes)", () => {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: "**Fast mode turned off:** requires extra usage to be enabled for this account.",
|
||||
text: "\n\n> **Fast mode turned off:** requires extra usage to be enabled for this account.\n\n",
|
||||
},
|
||||
});
|
||||
expect(sessionUpdates[1].update).toMatchObject({ sessionUpdate: "config_option_update" });
|
||||
|
||||
@@ -112,7 +112,7 @@ describe("session mode", () => {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: "**Auto mode unavailable:** the selected model does not support Auto mode; using Accept edits instead.",
|
||||
text: "\n\n> **Auto mode unavailable:** the selected model does not support Auto mode; using Accept edits instead.\n\n",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
clientSupportsNotices,
|
||||
collapseHookSpeaker,
|
||||
MAX_NOTICE_TITLE_LENGTH,
|
||||
normalizeNoticeText,
|
||||
noticeOrTranscriptUpdate,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
noticeTranscriptText,
|
||||
sentenceCase,
|
||||
splitNoticeText,
|
||||
transcriptBlock,
|
||||
} from "../session-notices.js";
|
||||
|
||||
describe("session notices", () => {
|
||||
@@ -30,21 +32,42 @@ describe("session notices", () => {
|
||||
expect(noticeTranscriptText({ severity: "info", title: "Done" })).toBe("**Done**");
|
||||
expect(noticeOrTranscriptUpdate(notice, false)).toEqual({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "**Fast mode turned off:** Why." },
|
||||
content: { type: "text", text: "\n\n> **Fast mode turned off:** Why.\n\n" },
|
||||
});
|
||||
expect(noticeOrTranscriptUpdate(notice, false, "custom line")).toEqual({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "custom line" },
|
||||
content: { type: "text", text: "\n\n> custom line\n\n" },
|
||||
});
|
||||
const meta = { claudeCode: { kind: "informational", level: "warning" } };
|
||||
expect(noticeOrTranscriptUpdate(notice, false, "custom line", meta)).toEqual({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "custom line" },
|
||||
content: { type: "text", text: "\n\n> custom line\n\n" },
|
||||
_meta: meta,
|
||||
});
|
||||
expect(noticeOrTranscriptUpdate(notice, true, "custom line", meta)).not.toHaveProperty("_meta");
|
||||
});
|
||||
|
||||
it("puts a transcript notice in its own blockquote paragraph", () => {
|
||||
// Without blank lines the chunk renders glued to Claude's last sentence:
|
||||
// "…all done.**Notice:** Stop says: …".
|
||||
expect(transcriptBlock("**Notice:** one line")).toBe("\n\n> **Notice:** one line\n\n");
|
||||
expect(transcriptBlock("**Notice:** first\nsecond\n\nfourth\n")).toBe(
|
||||
"\n\n> **Notice:** first\n> second\n>\n> fourth\n\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a hook's repeated speaker prefix on the first line only", () => {
|
||||
expect(collapseHookSpeaker("Stop says: token: 3 calls\nStop says: hand off now")).toBe(
|
||||
"Stop says: token: 3 calls\nhand off now",
|
||||
);
|
||||
expect(collapseHookSpeaker("Stop says: one line")).toBe("Stop says: one line");
|
||||
// Left alone unless every line repeats the same prefix.
|
||||
expect(collapseHookSpeaker("Stop says: a\nsomething else")).toBe(
|
||||
"Stop says: a\nsomething else",
|
||||
);
|
||||
expect(collapseHookSpeaker("no prefix\nStop says: b")).toBe("no prefix\nStop says: b");
|
||||
});
|
||||
|
||||
it("sends a notice update when the client can present one", () => {
|
||||
expect(
|
||||
noticeOrTranscriptUpdate({ severity: "info", title: "Task stopped by user" }, true),
|
||||
|
||||
Reference in New Issue
Block a user