Compare commits

..

4 Commits

Author SHA1 Message Date
Steffen Deusch
025038e8b5 wait for the first tool_use chunk in canUseTool
Before, if a subagent requested a tool permission, we'd send the
permission request with the tool_call_update before the initial
tool_call.
2026-06-25 16:35:14 +02:00
Ben Brandt
9616bdac47 feat: Add version flag handling (#813)
Closes #809
2026-06-25 12:37:02 +00:00
Ben Brandt
1fe7ec09a3 feat: Push session title updates at turn end (#812) 2026-06-25 11:45:50 +00:00
Ben Brandt
228f02ecfb feat(deps): Update @anthropic-ai/claude-agent-sdk to 0.3.191 (#810) 2026-06-25 13:42:57 +02:00
7 changed files with 439 additions and 72 deletions

107
package-lock.json generated
View File

@@ -9,18 +9,18 @@
"version": "0.51.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "0.29.0",
"@anthropic-ai/claude-agent-sdk": "0.3.187",
"@agentclientprotocol/sdk": "1.0.0",
"@anthropic-ai/claude-agent-sdk": "0.3.191",
"zod": "^3.25.0 || ^4.0.0"
},
"bin": {
"claude-agent-acp": "dist/index.js"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.105.0",
"@anthropic-ai/sdk": "0.106.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.5",
"@types/node": "26.0.0",
"@types/node": "26.0.1",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"eslint": "10.5.0",
@@ -36,31 +36,31 @@
}
},
"node_modules/@agentclientprotocol/sdk": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.29.0.tgz",
"integrity": "sha512-Sb8xJ/cIhqlvxeJiB0+1scBk0LAkVAwJ9WAFG9XCe3LEI5JWAyOxbqaCa8O1HmNnrk4+qHN11eaKH6zL2iHIYA==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.0.0.tgz",
"integrity": "sha512-5FHgLbLZhSAq9F+9SU7AOCXTDzaw/IYx/AvMjrZDs/YFZ9ZPxkdn9ti3fSkny0huHfj2hrqF6pqdns9gN5/Fig==",
"license": "Apache-2.0",
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.3.187",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.187.tgz",
"integrity": "sha512-HHkuC3he/Wi8fX/WjfS8mJr4TFPGoAd1QyxOuTwjnyOLboGkX1H+UhBYLxHX1ouFvHnYVJglB2wsuWemVQgahQ==",
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.191.tgz",
"integrity": "sha512-DM9oYbt+WAb/CiD8l/CO6akqPKfyZ4sL1e+lP2O2eS9IR0f58sf90xWeji7V+/zfnIp+jyH3hJ3V6r7odCdB8g==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.187",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.187",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.187",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.187",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.187",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.187",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.187",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.187"
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.191",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.191",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.191",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.191",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.191",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.191",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.191",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.191"
},
"peerDependencies": {
"@anthropic-ai/sdk": ">=0.93.0",
@@ -69,9 +69,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
"version": "0.3.187",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.187.tgz",
"integrity": "sha512-0DNzATzaRmjS/Q1T4T9SLP/VT67gRX7J4reYPnEdcTm91lJwjqOPkHr17+sv+7DoerZ421ZG38dPsQd/V67qQw==",
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.191.tgz",
"integrity": "sha512-rmpi5ONfuuw7oXdzyr9QJB5sK9Rr1MmdOUsiGutVFpcAqqSXpVYLmMvfaarmXvItgfwgGprzrGDfcAeAQHSUGw==",
"cpu": [
"arm64"
],
@@ -82,9 +82,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
"version": "0.3.187",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.187.tgz",
"integrity": "sha512-Va3O8lTDa9IHq/DbSJwU63JJknNV3YCBh4PEznOHXJtyFoXHgRjrks4QQvN3baZm79avVq2sn+6tvpTz9CuY8A==",
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.191.tgz",
"integrity": "sha512-b/4pfSFTC2gkc0RoGSJ5B3DWo77qZFMi2hRlx8kwaRDiefeIOnbJu42gDBrs4F8nSJJug77HM9AsG6XHXvt5Fg==",
"cpu": [
"x64"
],
@@ -95,9 +95,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
"version": "0.3.187",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.187.tgz",
"integrity": "sha512-VEvrs3MgwckdWRNa7+2rJdyOBbCWGoRaM6OnL+/n9qi4gKlZnXZlj/90Tw9o8ttcN260Opz120/SE1fYwzu/JA==",
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.191.tgz",
"integrity": "sha512-vm526e3InpItmh+Ua8a49eGTpFJvZTYLURX9ppKrtFb+Wo7rOgoXmK/Hzf/crEU7h72xtaD7PIOXm4P+psxO3w==",
"cpu": [
"arm64"
],
@@ -111,9 +111,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
"version": "0.3.187",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.187.tgz",
"integrity": "sha512-XQ7w2pX0mjPYUC7ayTKiHeAbePOny/ezBATxTWt5JVDZZPfljzvHA0jyXHsN8aLphbgRUfbUpdrtt5b57Bhx5g==",
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.191.tgz",
"integrity": "sha512-+Gg7lKwYDr8uiA37EBMYI63if+YGdbrFt2tcRFBB4IhdVf5r1IkKwQ8rEYV6Y0nqGTKoXLh3bRc5QWOe5JwgbQ==",
"cpu": [
"arm64"
],
@@ -127,9 +127,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
"version": "0.3.187",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.187.tgz",
"integrity": "sha512-s/Fmg29tzMrbdeCbseTpL3DlzfWineSQ3bDPvhdKw4jcsgLC1YgQhIkAyE8WlceUyhQyw82NAUgYoPwfGny6hQ==",
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.191.tgz",
"integrity": "sha512-wj43/l0Behb4UWYt6Lomq2W7jhmuU7ARsEP0el6gLGCR348PDeqLGcB4dMOcYMGRZv8TYKfnhDwJnX4pJBT1vQ==",
"cpu": [
"x64"
],
@@ -143,9 +143,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
"version": "0.3.187",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.187.tgz",
"integrity": "sha512-yi2/L5oztFqTDnAZl4mWOINr+r7WBot70gYUxG2jOoqdYUl6j50vG/ME605X80v+l2qBmKjSGxb5trY/6DkRzQ==",
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.191.tgz",
"integrity": "sha512-tUwwIXpPBrzJge3E4SQJQX8PuC+lxPb5xQBmyLsWxyjttlgpFpTG/jWPVQO5qH0HgYaEJS5JALHFiYcXwVrEkg==",
"cpu": [
"x64"
],
@@ -159,9 +159,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
"version": "0.3.187",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.187.tgz",
"integrity": "sha512-HjEl3cDRLAWKopdlrLI54fxTVWq4lZpWA4bTTBVc9UDdDj6AmJIRHKxaCCYLQRvnoswbAUF1sJmJ8EFJ+b6lfw==",
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.191.tgz",
"integrity": "sha512-J8aTOi7n6AM7tHFlKt6hlJ4rue0TbY2glqcI2r6K3iK7Fmzvt9zfg00+l8fWKOais4K+8jqJCC4WSCUlz6+tKQ==",
"cpu": [
"arm64"
],
@@ -172,9 +172,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
"version": "0.3.187",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.187.tgz",
"integrity": "sha512-FnrLhoZ8y/+gkZynL12UeQ8pWKCu6B/8myyRrYMNJRZqFw4DZlPvPCywAjFZf1e1hiXCoiSK0Orl5wzKfAeQsQ==",
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.191.tgz",
"integrity": "sha512-1rYlPuM9PS37IPCE/sAG7A7oCrWnIb0J7EY6eQBdImdXHEjwBngZ3E8KbtEM79fUTA3C810LqrnFpjOmNExLMA==",
"cpu": [
"x64"
],
@@ -185,9 +185,9 @@
]
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.105.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.105.0.tgz",
"integrity": "sha512-sDyu+aM9cE6uZE+HgRjjHRb+qqb87GHZOx+8bE0YlWetdL1YcVLxn8h9ltxGOflyChTe6PMEo50kMQV4cw0hfg==",
"version": "0.106.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.106.0.tgz",
"integrity": "sha512-ufwVvYNDBj2dzOGupBCTaNzBLxqcTnGOzI4z8Wouxlt+mT3J3HuOmatgCy1VmwCHOUueqZ41ERhm0O99OUcbWA==",
"license": "MIT",
"dependencies": {
"json-schema-to-ts": "^3.1.1",
@@ -934,9 +934,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.0.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
"integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
"version": "26.0.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz",
"integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -967,7 +967,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "8.62.0",
"@typescript-eslint/parser": "^8.62.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
@@ -3267,13 +3267,14 @@
}
},
"node_modules/qs": {
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"side-channel": "^1.1.0"
"es-define-property": "^1.0.1",
"side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"

View File

@@ -63,15 +63,15 @@
"node": ">=22"
},
"dependencies": {
"@agentclientprotocol/sdk": "0.29.0",
"@anthropic-ai/claude-agent-sdk": "0.3.187",
"@agentclientprotocol/sdk": "1.0.0",
"@anthropic-ai/claude-agent-sdk": "0.3.191",
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.105.0",
"@anthropic-ai/sdk": "0.106.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.5",
"@types/node": "26.0.0",
"@types/node": "26.0.1",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"eslint": "10.5.0",

View File

@@ -50,6 +50,7 @@ import {
AgentInfo,
CanUseTool,
deleteSession,
getSessionInfo,
getSessionMessages,
listSessions,
McpServerConfig,
@@ -262,12 +263,23 @@ type Session = {
/** Accumulated task list for the session, keyed by task ID. Task IDs are
* per-session, so this state must not be shared across sessions. */
taskState: TaskState;
/** Last session title we pushed to the client via `session_info_update`.
* The SDK auto-generates a title in a background task and persists it to the
* session file; we poll it on each turn-end (`session_state_changed: idle`)
* and only notify the client when it actually changes. Undefined until the
* first title is observed. */
lastTitle?: string;
/** Caches `tool_use` blocks by id so the matching `tool_result` can recover
* the tool name/input when mapping it to a `tool_call_update`. Per-session
* (tool_use ids are only unique within a session) and pruned at
* `tool_result` time so a long-running session doesn't accumulate every
* tool call for its whole lifetime. */
toolUseCache: ToolUseCache;
/**
* This lets canUseTool wait for the first tool call message from the SDK
* to ensure it emits a tool_call_update only after the first tool_call message.
*/
waitingForToolCall: WaitingForToolCall;
/** Maps the ACP `messageId` we expose to clients (see `messageIdForGrouping`)
* to the SDK message uuid that the Agent SDK's rewind/resume APIs key on
* (`Query.rewindFiles` takes a user-message uuid; `resumeSessionAt` takes an
@@ -382,6 +394,8 @@ export type ToolUpdateMeta = {
};
};
export type WaitingForToolCall = Map<string, () => void>;
export type ToolUseCache = {
[key: string]: {
type: "tool_use" | "server_tool_use" | "mcp_tool_use";
@@ -943,6 +957,40 @@ export class ClaudeAcpAgent {
};
}
/** Read the SDK-maintained title for a session and, if it changed since the
* last time we looked, notify the client with a `session_info_update`. The
* SDK has no push event for the title it auto-generates in the background, so
* we pull it at turn-end. A missing session file or read error is non-fatal:
* the title is best-effort and another turn will retry. */
private async maybeUpdateSessionTitle(sessionId: string, session: Session): Promise<void> {
let info;
try {
info = await getSessionInfo(sessionId, { dir: session.cwd });
} catch (error) {
this.logger.error(`Session ${sessionId}: failed to read session info: ${error}`);
return;
}
// `customTitle` is a user-set `/rename`; `summary` is the auto-generated
// title (or first prompt). Prefer the explicit title when present.
const rawTitle = info?.customTitle ?? info?.summary;
if (!rawTitle) {
return;
}
const title = sanitizeTitle(rawTitle);
if (title === session.lastTitle) {
return;
}
session.lastTitle = title;
await this.client.sessionUpdate({
sessionId,
update: {
sessionUpdate: "session_info_update",
title,
updatedAt: new Date(info!.lastModified).toISOString(),
},
});
}
async authenticate(_params: AuthenticateRequest): Promise<void> {
if (_params.methodId === "gateway" || _params.methodId === "gateway-bedrock") {
this.gatewayAuthRequest = _params as GatewayAuthRequest;
@@ -1365,6 +1413,11 @@ export class ClaudeAcpAgent {
if (session.cancelled) {
settleActive({ stopReason: "cancelled" });
}
// The SDK generates the session title in a background task and
// persists it to the session file; `idle` is the turn-over
// signal, so it's the point at which a new title may have
// landed. Push it to the client if it changed.
await this.maybeUpdateSessionTitle(params.sessionId, session);
}
break;
}
@@ -1643,6 +1696,7 @@ export class ClaudeAcpAgent {
"assistant",
params.sessionId,
session.toolUseCache,
session.waitingForToolCall,
this.client,
this.logger,
)) {
@@ -1813,6 +1867,7 @@ export class ClaudeAcpAgent {
message,
params.sessionId,
session.toolUseCache,
session.waitingForToolCall,
this.client,
this.logger,
{
@@ -1913,6 +1968,7 @@ export class ClaudeAcpAgent {
message.message.role,
params.sessionId,
session.toolUseCache,
session.waitingForToolCall,
this.client,
this.logger,
{
@@ -2043,6 +2099,7 @@ export class ClaudeAcpAgent {
message.message.role,
params.sessionId,
session.toolUseCache,
session.waitingForToolCall,
this.client,
this.logger,
{
@@ -2410,6 +2467,7 @@ export class ClaudeAcpAgent {
private async replaySessionHistory(sessionId: string): Promise<void> {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const messages = await getSessionMessages(sessionId);
for (const message of messages) {
@@ -2438,6 +2496,7 @@ export class ClaudeAcpAgent {
message.message.role,
sessionId,
toolUseCache,
waitingForToolCall,
this.client,
this.logger,
{
@@ -2486,8 +2545,6 @@ export class ClaudeAcpAgent {
canUseTool(sessionId: string): CanUseTool {
return async (toolName, toolInput, { signal, suggestions, toolUseID }) => {
const alwaysAllowLabel = describeAlwaysAllow(suggestions, toolName);
const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true;
const session = this.sessions[sessionId];
if (!session) {
return {
@@ -2495,6 +2552,13 @@ export class ClaudeAcpAgent {
message: "Session not found",
};
}
if (!session.toolUseCache[toolUseID]) {
const { promise, resolve } = Promise.withResolvers<void>();
session.waitingForToolCall.set(toolUseID, resolve);
await promise;
}
const alwaysAllowLabel = describeAlwaysAllow(suggestions, toolName);
const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true;
// AskUserQuestion is surfaced to us as a normal permission check (the SDK
// routes it through canUseTool whenever a callback is registered, rather
@@ -3355,6 +3419,7 @@ export class ClaudeAcpAgent {
) ?? DEFAULT_CONTEXT_WINDOW,
taskState,
toolUseCache: {},
waitingForToolCall: new Map(),
messageIdToUuid: new Map(),
};
@@ -4080,6 +4145,7 @@ export function toAcpNotifications(
role: "assistant" | "user",
sessionId: string,
toolUseCache: ToolUseCache,
waitingForToolCall: WaitingForToolCall,
client: AcpClient,
logger: Logger,
options?: {
@@ -4249,6 +4315,13 @@ export function toAcpNotifications(
...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd),
};
} else {
if (waitingForToolCall.has(chunk.id)) {
// If a canUseTool is waiting for the first tool_use chunk,
// we can resolve it now.
const resolve = waitingForToolCall.get(chunk.id)!;
resolve();
waitingForToolCall.delete(chunk.id);
}
// First encounter (streaming content_block_start or replay) —
// send as tool_call with terminal_info for Bash tools.
update = {
@@ -4406,6 +4479,7 @@ export function streamEventToAcpNotifications(
message: SDKPartialAssistantMessage,
sessionId: string,
toolUseCache: ToolUseCache,
waitingForToolCall: WaitingForToolCall,
client: AcpClient,
logger: Logger,
options?: {
@@ -4423,6 +4497,7 @@ export function streamEventToAcpNotifications(
"assistant",
sessionId,
toolUseCache,
waitingForToolCall,
client,
logger,
{
@@ -4439,6 +4514,7 @@ export function streamEventToAcpNotifications(
"assistant",
sessionId,
toolUseCache,
waitingForToolCall,
client,
logger,
{

View File

@@ -2,8 +2,12 @@
import { resolveSettings } from "@anthropic-ai/claude-agent-sdk";
import { claudeCliPath, runAcp } from "./acp-agent.js";
import packageJson from "../package.json" with { type: "json" };
if (process.argv.includes("--cli")) {
if (process.argv.includes("--version") || process.argv.includes("-v")) {
console.log(packageJson.version);
process.exit(0);
} else if (process.argv.includes("--cli")) {
const { spawn } = await import("node:child_process");
const args = process.argv.slice(2).filter((arg) => arg !== "--cli");
const child = spawn(await claudeCliPath(), args, { stdio: "inherit" });

View File

@@ -45,6 +45,7 @@ import {
import { Pushable } from "../utils.js";
import {
deleteSession,
getSessionInfo,
getSessionMessages,
query,
SDKAssistantMessage,
@@ -56,6 +57,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
return {
...actual,
deleteSession: vi.fn(),
getSessionInfo: vi.fn(),
};
});
import type {
@@ -116,6 +118,7 @@ function mockSessionState(overrides: Record<string, any> = {}) {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
waitingForToolCall: new Map(),
messageIdToUuid: new Map(),
...overrides,
} as any;
@@ -870,6 +873,7 @@ describe("tool conversions", () => {
received.message.role,
"test",
{},
new Map(),
{} as AcpClient,
console,
),
@@ -1812,11 +1816,51 @@ describe("permission request cancellation", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
waitingForToolCall: new Map(),
messageIdToUuid: new Map(),
} as any;
return agent.sessions[sessionId]!;
}
it("waits for the first tool_call before requesting permission", async () => {
const requestPermission = vi.fn(async () => ({
outcome: { outcome: "selected", optionId: "allow" },
}));
const mockClient = {
sessionUpdate: async () => {},
requestPermission,
} as unknown as AcpClient;
const agent = new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
const session = injectSession(agent, "session-1");
const pending = agent.canUseTool("session-1")("Bash", { command: "echo hello" }, {
signal: new AbortController().signal,
suggestions: [],
toolUseID: "tool-1",
} as any);
await Promise.resolve();
expect(requestPermission).not.toHaveBeenCalled();
expect(session.waitingForToolCall.has("tool-1")).toBe(true);
const notifications = toAcpNotifications(
[{ type: "tool_use", id: "tool-1", name: "Bash", input: { command: "echo hello" } }],
"assistant",
"session-1",
session.toolUseCache,
session.waitingForToolCall,
mockClient,
console,
);
expect(notifications[0].update.sessionUpdate).toBe("tool_call");
expect(requestPermission).not.toHaveBeenCalled();
await expect(pending).resolves.toMatchObject({ behavior: "allow" });
expect(requestPermission).toHaveBeenCalledTimes(1);
expect(session.waitingForToolCall.has("tool-1")).toBe(false);
});
it("forwards the tool-call signal so a pending permission request is cancelled on abort", async () => {
let receivedSignal: AbortSignal | undefined;
const mockClient = {
@@ -1833,7 +1877,13 @@ describe("permission request cancellation", () => {
},
} as unknown as AcpClient;
const agent = new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
injectSession(agent, "session-1");
const session = injectSession(agent, "session-1");
session.toolUseCache["tool-1"] = {
type: "tool_use",
id: "tool-1",
name: "Bash",
input: { command: "ls" },
};
const controller = new AbortController();
const pending = agent.canUseTool("session-1")("Bash", { command: "ls" }, {
@@ -1858,7 +1908,13 @@ describe("permission request cancellation", () => {
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
} as unknown as AcpClient;
const agent = new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
injectSession(agent, "session-1");
const session = injectSession(agent, "session-1");
session.toolUseCache["tool-1"] = {
type: "tool_use",
id: "tool-1",
name: "Bash",
input: { command: "ls" },
};
await expect(
agent.canUseTool("session-1")("Bash", { command: "ls" }, {
@@ -1927,6 +1983,12 @@ describe("runPromptWithCancellation", () => {
});
describe("stop reason propagation", () => {
// The title-update tests set `getSessionInfo` to resolve a title; reset it so
// that value can't leak into other turn-end (idle) assertions in this block.
beforeEach(() => {
vi.mocked(getSessionInfo).mockReset();
});
function createMockAgent() {
const mockClient = {
sessionUpdate: async () => {},
@@ -2322,6 +2384,98 @@ describe("stop reason propagation", () => {
expect(chunkTexts).toContain("between-turn background note");
});
it("pushes a session_info_update when the SDK generates a title at turn-end", async () => {
const sessionUpdates: any[] = [];
const mockClient = {
sessionUpdate: async (u: any) => {
sessionUpdates.push(u);
},
} as unknown as AcpClient;
const agent = new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
vi.mocked(getSessionInfo).mockResolvedValue({
sessionId: "test-session",
summary: "Fix the flaky title test",
lastModified: 1_700_000_000_000,
} as any);
const input = new Pushable<any>();
async function* messageGenerator() {
const iter = input[Symbol.asyncIterator]();
const { value: userMessage } = await iter.next();
yield userEcho(userMessage);
yield createResultMessage({ subtype: "success", stop_reason: "end_turn", is_error: false });
yield { type: "system", subtype: "session_state_changed", state: "idle" };
}
agent.sessions["test-session"] = mockSessionState({
query: wrapQuery(messageGenerator()),
input,
});
await agent.prompt({
sessionId: "test-session",
prompt: [{ type: "text", text: "test" }],
});
await agent.sessions["test-session"]?.consumer;
const titleUpdate = sessionUpdates.find(
(u) => u.update?.sessionUpdate === "session_info_update",
);
expect(titleUpdate?.update).toEqual({
sessionUpdate: "session_info_update",
title: "Fix the flaky title test",
updatedAt: new Date(1_700_000_000_000).toISOString(),
});
expect(getSessionInfo).toHaveBeenCalledWith("test-session", { dir: "/test" });
});
it("does not re-push session_info_update when the title is unchanged", async () => {
const sessionUpdates: any[] = [];
const mockClient = {
sessionUpdate: async (u: any) => {
sessionUpdates.push(u);
},
} as unknown as AcpClient;
const agent = new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
vi.mocked(getSessionInfo).mockResolvedValue({
sessionId: "test-session",
summary: "Stable title",
lastModified: 1_700_000_000_000,
} as any);
const input = new Pushable<any>();
async function* messageGenerator() {
const iter = input[Symbol.asyncIterator]();
// Two turns, each ending in idle, but the title never changes.
for (let i = 0; i < 2; i++) {
const { value: userMessage } = await iter.next();
yield userEcho(userMessage);
yield createResultMessage({
subtype: "success",
stop_reason: "end_turn",
is_error: false,
});
yield { type: "system", subtype: "session_state_changed", state: "idle" };
}
}
agent.sessions["test-session"] = mockSessionState({
query: wrapQuery(messageGenerator()),
input,
});
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "one" }] });
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "two" }] });
await agent.sessions["test-session"]?.consumer;
const titleUpdates = sessionUpdates.filter(
(u) => u.update?.sessionUpdate === "session_info_update",
);
expect(titleUpdates).toHaveLength(1);
});
it("should throw internal error for success with is_error true and no max_tokens", async () => {
const agent = createMockAgent();
injectSession(agent, [
@@ -2464,6 +2618,7 @@ describe("session/close", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
waitingForToolCall: new Map(),
messageIdToUuid: new Map(),
};
return agent.sessions[sessionId]!;
@@ -2549,6 +2704,7 @@ describe("session/delete", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
waitingForToolCall: new Map(),
messageIdToUuid: new Map(),
};
return agent.sessions[sessionId]!;
@@ -2651,6 +2807,7 @@ describe("getOrCreateSession param change detection", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
waitingForToolCall: new Map(),
messageIdToUuid: new Map(),
};
return agent.sessions[sessionId]!;
@@ -4865,6 +5022,7 @@ describe("post-error recovery", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
waitingForToolCall: new Map(),
messageIdToUuid: new Map(),
};
return { interrupt };
@@ -5435,6 +5593,7 @@ describe("session/cancel wedge recovery (issue #680)", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
waitingForToolCall: new Map(),
messageIdToUuid: new Map(),
};
return { interrupt };
@@ -5566,9 +5725,8 @@ describe("streamEventToAcpNotifications", () => {
pingMessage,
"test-session",
{},
{ sessionUpdate: async () => {} } as unknown as Parameters<
typeof streamEventToAcpNotifications
>[3],
new Map(),
{ sessionUpdate: async () => {} } as unknown as AcpClient,
logger,
);
@@ -5590,9 +5748,17 @@ describe("streamEventToAcpNotifications", () => {
},
} as Parameters<typeof streamEventToAcpNotifications>[0];
const result = streamEventToAcpNotifications(message, "test", {}, {} as AcpClient, console, {
messageId,
});
const result = streamEventToAcpNotifications(
message,
"test",
{},
new Map(),
{} as AcpClient,
console,
{
messageId,
},
);
expect(result).toEqual([
{
@@ -5616,6 +5782,7 @@ describe("toAcpNotifications messageId", () => {
"assistant",
"test",
{},
new Map(),
{} as AcpClient,
console,
{ messageId },
@@ -5639,6 +5806,7 @@ describe("toAcpNotifications messageId", () => {
"user",
"test",
{},
new Map(),
{} as AcpClient,
console,
{ messageId },
@@ -5653,6 +5821,7 @@ describe("toAcpNotifications messageId", () => {
"assistant",
"test",
{},
new Map(),
{} as AcpClient,
console,
{ messageId },
@@ -5664,7 +5833,15 @@ describe("toAcpNotifications messageId", () => {
});
it("omits messageId when none is supplied", () => {
const result = toAcpNotifications("hello", "assistant", "test", {}, {} as AcpClient, console);
const result = toAcpNotifications(
"hello",
"assistant",
"test",
{},
new Map(),
{} as AcpClient,
console,
);
expect(result[0].update).not.toHaveProperty("messageId");
});
@@ -5681,6 +5858,7 @@ describe("toAcpNotifications messageId", () => {
"assistant",
"test",
{},
new Map(),
{} as AcpClient,
console,
{ messageId, registerHooks: false },
@@ -5697,6 +5875,7 @@ describe("toAcpNotifications thinking chunks", () => {
"assistant",
"test",
{},
new Map(),
{} as AcpClient,
console,
);
@@ -5718,6 +5897,7 @@ describe("toAcpNotifications thinking chunks", () => {
"assistant",
"test",
{},
new Map(),
{} as AcpClient,
console,
);
@@ -5731,6 +5911,7 @@ describe("toAcpNotifications thinking chunks", () => {
"assistant",
"test",
{},
new Map(),
{} as AcpClient,
console,
);
@@ -5888,6 +6069,7 @@ describe("agent selection config option", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
waitingForToolCall: new Map(),
messageIdToUuid: new Map(),
};
return { session: agent.sessions[sessionId]!, applyFlagSettings };

View File

@@ -124,6 +124,8 @@ describe("session config options", () => {
),
configOptions: structuredClone(MOCK_CONFIG_OPTIONS),
contextWindowSize: 200000,
toolUseCache: {},
waitingForToolCall: new Map(),
};
}
@@ -1112,6 +1114,12 @@ describe("session config options", () => {
{ id: "dontAsk", name: "Don't Ask", description: "Deny if not pre-approved" },
],
};
session.toolUseCache["toolu_1"] = {
type: "tool_use",
id: "toolu_1",
name: "ExitPlanMode",
input: { plan: "do stuff" },
};
const canUseTool = (agent as any).canUseTool(SESSION_ID);
const signal = new AbortController().signal;
@@ -1144,6 +1152,12 @@ describe("session config options", () => {
],
};
permissionResponse = { outcome: { outcome: "selected", optionId: "auto" } };
session.toolUseCache["toolu_2"] = {
type: "tool_use",
id: "toolu_2",
name: "ExitPlanMode",
input: { plan: "do stuff" },
};
const canUseTool = (agent as any).canUseTool(SESSION_ID);
const result = await canUseTool(
@@ -1171,6 +1185,12 @@ describe("session config options", () => {
{ id: "dontAsk", name: "Don't Ask", description: "Deny if not pre-approved" },
],
};
session.toolUseCache["toolu_3"] = {
type: "tool_use",
id: "toolu_3",
name: "ExitPlanMode",
input: { plan: "do stuff" },
};
const canUseTool = (agent as any).canUseTool(SESSION_ID);
const signal = new AbortController().signal;

View File

@@ -10,7 +10,13 @@ import {
BetaBashCodeExecutionResultBlock,
BetaBashCodeExecutionToolResultBlockParam,
} from "@anthropic-ai/sdk/resources/beta.mjs";
import { AcpClient, toAcpNotifications, ToolUseCache, Logger } from "../acp-agent.js";
import {
AcpClient,
toAcpNotifications,
ToolUseCache,
Logger,
WaitingForToolCall,
} from "../acp-agent.js";
import {
toolUpdateFromToolResult,
createPostToolUseHook,
@@ -37,6 +43,7 @@ describe("rawOutput in tool call updates", () => {
input: { command: "echo hello" },
},
};
const waitingForToolCall: WaitingForToolCall = new Map();
const toolResult: ToolResultBlockParam = {
type: "tool_result",
@@ -50,6 +57,7 @@ describe("rawOutput in tool call updates", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -72,6 +80,7 @@ describe("rawOutput in tool call updates", () => {
input: { file_path: "/test/file.txt" },
},
};
const waitingForToolCall: WaitingForToolCall = new Map();
// ToolResultBlockParam content can be string or array of TextBlockParam
const toolResult: ToolResultBlockParam = {
@@ -86,6 +95,7 @@ describe("rawOutput in tool call updates", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -108,6 +118,7 @@ describe("rawOutput in tool call updates", () => {
input: { query: "test" },
},
};
const waitingForToolCall: WaitingForToolCall = new Map();
// BetaMCPToolResultBlock content can be string or Array<BetaTextBlock>
const toolResult: BetaMCPToolResultBlock = {
@@ -122,6 +133,7 @@ describe("rawOutput in tool call updates", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -144,6 +156,7 @@ describe("rawOutput in tool call updates", () => {
input: { term: "test" },
},
};
const waitingForToolCall: WaitingForToolCall = new Map();
// BetaTextBlock requires citations field
const arrayContent: BetaTextBlock[] = [
@@ -163,6 +176,7 @@ describe("rawOutput in tool call updates", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -185,6 +199,7 @@ describe("rawOutput in tool call updates", () => {
input: { query: "test search" },
},
};
const waitingForToolCall: WaitingForToolCall = new Map();
// BetaWebSearchResultBlock from SDK
const searchResults: BetaWebSearchResultBlock[] = [
@@ -208,6 +223,7 @@ describe("rawOutput in tool call updates", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -230,6 +246,7 @@ describe("rawOutput in tool call updates", () => {
input: { command: "ls -la" },
},
};
const waitingForToolCall: WaitingForToolCall = new Map();
// BetaBashCodeExecutionResultBlock from SDK
const bashResult: BetaBashCodeExecutionResultBlock = {
@@ -251,6 +268,7 @@ describe("rawOutput in tool call updates", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -273,6 +291,7 @@ describe("rawOutput in tool call updates", () => {
input: { command: "invalid_command" },
},
};
const waitingForToolCall: WaitingForToolCall = new Map();
const toolResult: ToolResultBlockParam = {
type: "tool_result",
@@ -286,6 +305,7 @@ describe("rawOutput in tool call updates", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -308,6 +328,7 @@ describe("rawOutput in tool call updates", () => {
input: { todos: [{ content: "Test task", status: "pending" }] },
},
};
const waitingForToolCall: WaitingForToolCall = new Map();
const toolResult: ToolResultBlockParam = {
type: "tool_result",
@@ -321,6 +342,7 @@ describe("rawOutput in tool call updates", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -351,11 +373,14 @@ describe("rawOutput in tool call updates", () => {
is_error: false,
};
const waitingForToolCall: WaitingForToolCall = new Map();
const notifications = toAcpNotifications(
[toolResult],
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -383,6 +408,7 @@ describe("rawOutput in tool call updates", () => {
input: { file_path: "/test/image.png" },
},
};
const waitingForToolCall: WaitingForToolCall = new Map();
const imageBlock: ImageBlockParam = {
type: "image",
@@ -401,6 +427,7 @@ describe("rawOutput in tool call updates", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -779,6 +806,7 @@ describe("Bash terminal output", () => {
// Reset before each test: toAcpNotifications prunes the cache entry once it
// maps the tool_result, so a shared object would be empty by the 2nd test.
let toolUseCache: ToolUseCache;
const waitingForToolCall: WaitingForToolCall = new Map();
beforeEach(() => {
toolUseCache = {
toolu_bash: {
@@ -814,6 +842,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
{ clientCapabilities },
@@ -854,6 +883,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -880,6 +910,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
{ clientCapabilities },
@@ -895,6 +926,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
{ clientCapabilities: { _meta: { terminal_output: true } } },
@@ -915,6 +947,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCacheWithoutSupport,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -947,6 +980,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
{ clientCapabilities },
@@ -969,6 +1003,7 @@ describe("Bash terminal output", () => {
describe("toolUseCache pruning", () => {
it("retains the tool_use entry until its result, then prunes it", () => {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const toolUse = {
type: "tool_use" as const,
id: "toolu_read",
@@ -977,7 +1012,15 @@ describe("Bash terminal output", () => {
};
// tool_use is cached and kept (the matching result hasn't arrived yet).
toAcpNotifications([toolUse], "assistant", "s", toolUseCache, mockClient, mockLogger);
toAcpNotifications(
[toolUse],
"assistant",
"s",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
expect(toolUseCache.toolu_read).toBeDefined();
// tool_result resolves it, so the entry is pruned to bound memory.
@@ -986,7 +1029,15 @@ describe("Bash terminal output", () => {
tool_use_id: "toolu_read",
content: [{ type: "text", text: "hello" }],
};
toAcpNotifications([toolResult], "assistant", "s", toolUseCache, mockClient, mockLogger);
toAcpNotifications(
[toolResult],
"assistant",
"s",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
expect(toolUseCache.toolu_read).toBeUndefined();
});
});
@@ -994,6 +1045,7 @@ describe("Bash terminal output", () => {
describe("post-tool-use hook sends diff content for Edit tool", () => {
it("should include content and locations from structuredPatch in hook update", async () => {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const hookUpdates: any[] = [];
const mockClientWithUpdate = {
@@ -1019,6 +1071,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClientWithUpdate,
mockLogger,
);
@@ -1073,6 +1126,7 @@ describe("Bash terminal output", () => {
it("should include multiple diff blocks for replaceAll with multiple hunks", async () => {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const hookUpdates: any[] = [];
const mockClientWithUpdate = {
@@ -1098,6 +1152,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClientWithUpdate,
mockLogger,
);
@@ -1158,6 +1213,7 @@ describe("Bash terminal output", () => {
it("should not include content/locations for non-Edit tools", async () => {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const hookUpdates: any[] = [];
const mockClientWithUpdate = {
@@ -1178,6 +1234,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClientWithUpdate,
mockLogger,
);
@@ -1213,6 +1270,7 @@ describe("Bash terminal output", () => {
// tool_use time and was never corrected after the tool ran.
it("should emit a real diff for Write of an existing file (type: update)", async () => {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const hookUpdates: any[] = [];
const mockClientWithUpdate = {
sessionUpdate: async (notification: any) => {
@@ -1235,6 +1293,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClientWithUpdate,
mockLogger,
);
@@ -1289,6 +1348,7 @@ describe("Bash terminal output", () => {
it("should still emit a sensible diff for Write of a brand-new file (type: create)", async () => {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const hookUpdates: any[] = [];
const mockClientWithUpdate = {
sessionUpdate: async (notification: any) => {
@@ -1311,6 +1371,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClientWithUpdate,
mockLogger,
);
@@ -1368,6 +1429,7 @@ describe("Bash terminal output", () => {
};
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
// Capture session updates sent by the hook callback
const hookUpdates: any[] = [];
@@ -1389,6 +1451,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClientWithUpdate,
mockLogger,
{ clientCapabilities },
@@ -1418,6 +1481,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClientWithUpdate,
mockLogger,
{ clientCapabilities },
@@ -1469,6 +1533,7 @@ describe("Bash terminal output", () => {
it("should not include terminal _meta in hook update when client lacks terminal_output support", async () => {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const hookUpdates: any[] = [];
const mockClientWithUpdate = {
@@ -1490,6 +1555,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClientWithUpdate,
mockLogger,
// No clientCapabilities — terminal_output not supported
@@ -1514,6 +1580,7 @@ describe("Bash terminal output", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClientWithUpdate,
mockLogger,
);
@@ -1645,6 +1712,7 @@ describe("toAcpNotifications - TodoWrite with undefined input regression", () =>
it("should not throw when TodoWrite tool_use has undefined input", () => {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const notifications = toAcpNotifications(
[
@@ -1658,6 +1726,7 @@ describe("toAcpNotifications - TodoWrite with undefined input regression", () =>
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -1669,6 +1738,7 @@ describe("toAcpNotifications - TodoWrite with undefined input regression", () =>
it("should still emit plan update when TodoWrite has valid input", () => {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const notifications = toAcpNotifications(
[
@@ -1682,6 +1752,7 @@ describe("toAcpNotifications - TodoWrite with undefined input regression", () =>
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
);
@@ -1780,6 +1851,7 @@ describe("toAcpNotifications - Task* tools", () => {
it("suppresses tool_call for TaskCreate/TaskUpdate/TaskList/TaskGet on tool_use", () => {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const taskState: TaskState = new Map();
const notifications = toAcpNotifications(
@@ -1797,6 +1869,7 @@ describe("toAcpNotifications - Task* tools", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
{ taskState },
@@ -1808,6 +1881,7 @@ describe("toAcpNotifications - Task* tools", () => {
it("emits a plan snapshot after a TaskCreate tool_result and accumulates state", () => {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const taskState: TaskState = new Map();
toAcpNotifications(
@@ -1822,6 +1896,7 @@ describe("toAcpNotifications - Task* tools", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
{ taskState },
@@ -1839,6 +1914,7 @@ describe("toAcpNotifications - Task* tools", () => {
"user",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
{ taskState },
@@ -1863,6 +1939,7 @@ describe("toAcpNotifications - Task* tools", () => {
"assistant",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
{ taskState },
@@ -1880,6 +1957,7 @@ describe("toAcpNotifications - Task* tools", () => {
"user",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
{ taskState },
@@ -1896,6 +1974,7 @@ describe("toAcpNotifications - Task* tools", () => {
it("emits a plan snapshot reflecting status changes after a TaskUpdate tool_result", () => {
const toolUseCache: ToolUseCache = {};
const waitingForToolCall: WaitingForToolCall = new Map();
const taskState: TaskState = new Map([["1", { subject: "First", status: "pending" as const }]]);
toolUseCache["update-1"] = {
type: "tool_use",
@@ -1920,6 +1999,7 @@ describe("toAcpNotifications - Task* tools", () => {
"user",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
{ taskState },
@@ -1937,6 +2017,7 @@ describe("toAcpNotifications - Task* tools", () => {
"list-1": { type: "tool_use", id: "list-1", name: "TaskList", input: {} },
"get-1": { type: "tool_use", id: "get-1", name: "TaskGet", input: { taskId: "1" } },
};
const waitingForToolCall: WaitingForToolCall = new Map();
const taskState: TaskState = new Map([
["1", { subject: "Existing", status: "in_progress" as const }],
]);
@@ -1949,6 +2030,7 @@ describe("toAcpNotifications - Task* tools", () => {
"user",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
{ taskState },
@@ -1967,6 +2049,7 @@ describe("toAcpNotifications - Task* tools", () => {
input: { subject: "A", description: "" },
},
};
const waitingForToolCall: WaitingForToolCall = new Map();
const taskState: TaskState = new Map();
const notifications = toAcpNotifications(
@@ -1981,6 +2064,7 @@ describe("toAcpNotifications - Task* tools", () => {
"user",
"test-session",
toolUseCache,
waitingForToolCall,
mockClient,
mockLogger,
{ taskState },