mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-04 16:31:56 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
505f9c13c0 | ||
|
|
f37e9a0d47 | ||
|
|
2162d5af62 | ||
|
|
798844726f |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.35.0"
|
||||
".": "0.36.0"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## [0.36.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.35.0...v0.36.0) (2026-05-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add experimental session delete support ([#682](https://github.com/agentclientprotocol/claude-agent-acp/issues/682)) ([2162d5a](https://github.com/agentclientprotocol/claude-agent-acp/commit/2162d5af62d493a82381bd88bc8fc67d376e358b))
|
||||
* Support experimental additionalDirectories field ([#684](https://github.com/agentclientprotocol/claude-agent-acp/issues/684)) ([f37e9a0](https://github.com/agentclientprotocol/claude-agent-acp/commit/f37e9a0d47d8d201d98b2177083664a6781895cc))
|
||||
|
||||
## [0.35.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.34.1...v0.35.0) (2026-05-16)
|
||||
|
||||
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.35.0",
|
||||
"version": "0.36.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.35.0",
|
||||
"version": "0.36.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.21.1",
|
||||
"@agentclientprotocol/sdk": "0.22.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.143",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
@@ -32,9 +32,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@agentclientprotocol/sdk": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.21.1.tgz",
|
||||
"integrity": "sha512-ZTLH+o9QxcZDLX/9ww+W7C2iExnXFM+vD/uGFVSlR61Kzj9FaxUqBC6Rv/kwgA7qVWYUEI9c5ZNqCuO9PM4rKg==",
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.22.0.tgz",
|
||||
"integrity": "sha512-mnkpiuEfGm7/snhWbrRhe3Hk+WDDjv7U2N/Ql0XiThl9zg7T984dPH8Tich4wlBQnaaXTV4Fl5OQSe2pa9hoFA==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.35.0",
|
||||
"version": "0.36.0",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
@@ -60,7 +60,7 @@
|
||||
"author": "Zed Industries",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.21.1",
|
||||
"@agentclientprotocol/sdk": "0.22.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.143",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
SetSessionModeResponse,
|
||||
CloseSessionRequest,
|
||||
CloseSessionResponse,
|
||||
DeleteSessionRequest,
|
||||
DeleteSessionResponse,
|
||||
TerminalHandle,
|
||||
TerminalOutputResponse,
|
||||
WriteTextFileRequest,
|
||||
@@ -45,6 +47,7 @@ import {
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import {
|
||||
CanUseTool,
|
||||
deleteSession,
|
||||
getSessionMessages,
|
||||
listSessions,
|
||||
McpServerConfig,
|
||||
@@ -625,10 +628,12 @@ export class ClaudeAcpAgent implements Agent {
|
||||
},
|
||||
loadSession: true,
|
||||
sessionCapabilities: {
|
||||
additionalDirectories: {},
|
||||
close: {},
|
||||
delete: {},
|
||||
fork: {},
|
||||
list: {},
|
||||
resume: {},
|
||||
close: {},
|
||||
},
|
||||
},
|
||||
agentInfo: {
|
||||
@@ -660,6 +665,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
{
|
||||
cwd: params.cwd,
|
||||
mcpServers: params.mcpServers ?? [],
|
||||
additionalDirectories: params.additionalDirectories,
|
||||
_meta: params._meta,
|
||||
},
|
||||
{
|
||||
@@ -1116,26 +1122,15 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
}
|
||||
|
||||
// Slash commands like /compact can generate invalid output... doesn't match
|
||||
// their own docs: https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-slash-commands#%2Fcompact-compact-conversation-history
|
||||
//
|
||||
// Strip local-command marker tags from the content and render whatever
|
||||
// real prose remains, so that custom slash commands / user-defined
|
||||
// skills (whose bodies arrive wrapped in <command-*> / <local-command-stdout>
|
||||
// markers) and built-in commands that emit textual output reach the UI.
|
||||
// Previously this branch dropped every message containing a stdout
|
||||
// marker, which silently swallowed every successful skill invocation
|
||||
// and any built-in command that emits output through these tags.
|
||||
// strip-and-render is safe because stripLocalCommandMetadata returns
|
||||
// null when nothing renderable remains (e.g. pure-marker /compact
|
||||
// payloads), so the no-render no-op path is preserved for those cases.
|
||||
//
|
||||
// Refs zed-industries/claude-code-acp#624, #642.
|
||||
// Strip <command-*>/<local-command-stdout> markers and render any
|
||||
// remaining prose. Skill bodies and built-in slash commands (e.g.
|
||||
// /usage, /status, /model) arrive wrapped in these tags; pure-marker
|
||||
// payloads (e.g. /compact's malformed output) strip to null and are
|
||||
// skipped. Mirrors the replay path at replaySessionHistory.
|
||||
if (
|
||||
typeof message.message.content === "string" &&
|
||||
message.message.content.includes("<local-command-stdout>")
|
||||
) {
|
||||
this.logger.log(message.message.content);
|
||||
const stripped = stripLocalCommandMetadata(message.message.content);
|
||||
if (typeof stripped === "string") {
|
||||
for (const notification of toAcpNotifications(
|
||||
@@ -1145,9 +1140,17 @@ export class ClaudeAcpAgent implements Agent {
|
||||
this.toolUseCache,
|
||||
this.client,
|
||||
this.logger,
|
||||
{
|
||||
clientCapabilities: this.clientCapabilities,
|
||||
parentToolUseId: message.parent_tool_use_id,
|
||||
cwd: session.cwd,
|
||||
taskState: session.taskState,
|
||||
},
|
||||
)) {
|
||||
await this.client.sessionUpdate(notification);
|
||||
}
|
||||
} else {
|
||||
this.logger.log(message.message.content);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1300,6 +1303,16 @@ export class ClaudeAcpAgent implements Agent {
|
||||
return {};
|
||||
}
|
||||
|
||||
async unstable_deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
|
||||
// Tear down any active in-memory state first so the on-disk file isn't
|
||||
// recreated by an outstanding query writing to it.
|
||||
if (this.sessions[params.sessionId]) {
|
||||
await this.teardownSession(params.sessionId);
|
||||
}
|
||||
await deleteSession(params.sessionId);
|
||||
return {};
|
||||
}
|
||||
|
||||
async unstable_setSessionModel(
|
||||
params: SetSessionModelRequest,
|
||||
): Promise<SetSessionModelResponse | void> {
|
||||
@@ -1764,6 +1777,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
sessionId: string;
|
||||
cwd: string;
|
||||
mcpServers?: NewSessionRequest["mcpServers"];
|
||||
additionalDirectories?: NewSessionRequest["additionalDirectories"];
|
||||
_meta?: NewSessionRequest["_meta"];
|
||||
}): Promise<NewSessionResponse> {
|
||||
const existingSession = this.sessions[params.sessionId];
|
||||
@@ -1788,6 +1802,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
{
|
||||
cwd: params.cwd,
|
||||
mcpServers: params.mcpServers ?? [],
|
||||
additionalDirectories: params.additionalDirectories,
|
||||
_meta: params._meta,
|
||||
},
|
||||
{
|
||||
@@ -1837,7 +1852,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
? Object.fromEntries(server.headers.map((e) => [e.name, e.value]))
|
||||
: undefined,
|
||||
};
|
||||
} else {
|
||||
} else if (!("type" in server)) {
|
||||
// Stdio type MCP server (with or without explicit type field)
|
||||
mcpServers[server.name] = {
|
||||
type: "stdio",
|
||||
@@ -2009,9 +2024,15 @@ export class ClaudeAcpAgent implements Agent {
|
||||
abortController,
|
||||
};
|
||||
|
||||
// Prefer the official ACP `additionalDirectories` field. Fall back to the
|
||||
// legacy `_meta.additionalRoots` extension for clients that haven't been
|
||||
// updated yet. Either source is merged with directories supplied via
|
||||
// `_meta.claudeCode.options.additionalDirectories` (SDK pass-through).
|
||||
const acpAdditionalDirectories =
|
||||
params.additionalDirectories ?? sessionMeta?.additionalRoots ?? [];
|
||||
options.additionalDirectories = [
|
||||
...(userProvidedOptions?.additionalDirectories ?? []),
|
||||
...(sessionMeta?.additionalRoots ?? []),
|
||||
...acpAdditionalDirectories,
|
||||
];
|
||||
|
||||
if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from "vitest";
|
||||
import { spawn, spawnSync } from "child_process";
|
||||
import {
|
||||
Agent,
|
||||
@@ -35,8 +35,16 @@ import {
|
||||
type SDKMessageFilter,
|
||||
} from "../acp-agent.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { query, SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { deleteSession, query, SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>();
|
||||
return {
|
||||
...actual,
|
||||
deleteSession: vi.fn(),
|
||||
};
|
||||
});
|
||||
import type {
|
||||
BetaToolResultBlockParam,
|
||||
BetaToolSearchToolResultBlockParam,
|
||||
@@ -1888,6 +1896,94 @@ describe("session/close", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("session/delete", () => {
|
||||
function createMockAgent() {
|
||||
const mockClient = {
|
||||
sessionUpdate: async () => {},
|
||||
} as unknown as AgentSideConnection;
|
||||
return new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
|
||||
}
|
||||
|
||||
function injectSession(agent: ClaudeAcpAgent, sessionId: string) {
|
||||
function* empty() {}
|
||||
const gen = Object.assign(empty(), { interrupt: vi.fn(), close: vi.fn() });
|
||||
agent.sessions[sessionId] = {
|
||||
query: gen as any,
|
||||
input: new Pushable(),
|
||||
cancelled: false,
|
||||
cwd: "/test",
|
||||
sessionFingerprint: JSON.stringify({ cwd: "/test", mcpServers: [] }),
|
||||
modes: { currentModeId: "default", availableModes: [] },
|
||||
models: { currentModelId: "default", availableModels: [] },
|
||||
modelInfos: [],
|
||||
settingsManager: { dispose: vi.fn() } as any,
|
||||
accumulatedUsage: {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cachedReadTokens: 0,
|
||||
cachedWriteTokens: 0,
|
||||
},
|
||||
configOptions: [],
|
||||
promptRunning: false,
|
||||
pendingMessages: new Map(),
|
||||
nextPendingOrder: 0,
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
};
|
||||
return agent.sessions[sessionId]!;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(deleteSession).mockReset();
|
||||
vi.mocked(deleteSession).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("tears down the active session and deletes it from disk", async () => {
|
||||
const agent = createMockAgent();
|
||||
const session = injectSession(agent, "session-1");
|
||||
|
||||
const result = await agent.unstable_deleteSession({ sessionId: "session-1" });
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(agent.sessions["session-1"]).toBeUndefined();
|
||||
expect(session.query.interrupt).toHaveBeenCalled();
|
||||
expect(session.settingsManager.dispose).toHaveBeenCalled();
|
||||
expect(session.abortController.signal.aborted).toBe(true);
|
||||
expect(deleteSession).toHaveBeenCalledWith("session-1");
|
||||
});
|
||||
|
||||
it("deletes a session from disk that is not currently active", async () => {
|
||||
const agent = createMockAgent();
|
||||
|
||||
const result = await agent.unstable_deleteSession({ sessionId: "not-active" });
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(deleteSession).toHaveBeenCalledWith("not-active");
|
||||
});
|
||||
|
||||
it("propagates errors from the SDK delete call", async () => {
|
||||
const agent = createMockAgent();
|
||||
vi.mocked(deleteSession).mockRejectedValueOnce(new Error("Session not found on disk"));
|
||||
|
||||
await expect(agent.unstable_deleteSession({ sessionId: "missing" })).rejects.toThrow(
|
||||
"Session not found on disk",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not affect other sessions when deleting one", async () => {
|
||||
const agent = createMockAgent();
|
||||
injectSession(agent, "session-a");
|
||||
injectSession(agent, "session-b");
|
||||
|
||||
await agent.unstable_deleteSession({ sessionId: "session-a" });
|
||||
|
||||
expect(agent.sessions["session-a"]).toBeUndefined();
|
||||
expect(agent.sessions["session-b"]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrCreateSession param change detection", () => {
|
||||
function createMockAgent() {
|
||||
const mockClient = {
|
||||
|
||||
@@ -76,4 +76,33 @@ describe("additionalRoots", () => {
|
||||
});
|
||||
expect(capturedOptions!.additionalDirectories).toEqual(["/workspace/shared", "", root]);
|
||||
});
|
||||
|
||||
it("prefers the official ACP additionalDirectories field over _meta.additionalRoots", async () => {
|
||||
await agent.newSession({
|
||||
cwd: "/test",
|
||||
mcpServers: [],
|
||||
additionalDirectories: ["/from/official"],
|
||||
_meta: { additionalRoots: ["/from/meta"] },
|
||||
});
|
||||
expect(capturedOptions!.additionalDirectories).toEqual(["/from/official"]);
|
||||
});
|
||||
|
||||
it("merges official ACP additionalDirectories with claudeCode SDK additionalDirectories", async () => {
|
||||
await agent.newSession({
|
||||
cwd: "/test",
|
||||
mcpServers: [],
|
||||
additionalDirectories: ["/from/official"],
|
||||
_meta: { claudeCode: { options: { additionalDirectories: ["/from/sdk"] } } },
|
||||
});
|
||||
expect(capturedOptions!.additionalDirectories).toEqual(["/from/sdk", "/from/official"]);
|
||||
});
|
||||
|
||||
it("falls back to _meta.additionalRoots when the official field is omitted", async () => {
|
||||
await agent.newSession({
|
||||
cwd: "/test",
|
||||
mcpServers: [],
|
||||
_meta: { additionalRoots: ["/from/meta"] },
|
||||
});
|
||||
expect(capturedOptions!.additionalDirectories).toEqual(["/from/meta"]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user