Skip to content

Commit 003ae41

Browse files
agu-zcole-miller
andauthored
Send available commands over notifications (#20)
See agentclientprotocol/agent-client-protocol#62 --------- Co-authored-by: Cole Miller <cole@zed.dev>
1 parent 0ba0ec1 commit 003ae41

4 files changed

Lines changed: 49 additions & 32 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"@anthropic-ai/claude-code": "^1.0.100",
4747
"@modelcontextprotocol/sdk": "^1.17.4",
4848
"@types/express": "^5.0.3",
49-
"@zed-industries/agent-client-protocol": "0.2.0-alpha.4",
49+
"@zed-industries/agent-client-protocol": "0.2.0-alpha.6",
5050
"diff": "^8.0.2",
5151
"express": "^5.1.0",
5252
"minimist": "^1.2.8",

src/acp-agent.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import * as fs from "node:fs";
3131
import * as path from "node:path";
3232
import * as os from "node:os";
3333
import { v7 as uuidv7 } from "uuid";
34-
import { nodeToWebReadable, nodeToWebWritable, Pushable, sleep, unreachable } from "./utils.js";
34+
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
3535
import { SessionNotification } from "@zed-industries/agent-client-protocol";
3636
import { createMcpServer } from "./mcp-server.js";
3737
import { AddressInfo } from "node:net";
@@ -102,6 +102,13 @@ export class ClaudeAcpAgent implements Agent {
102102
};
103103
}
104104
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
105+
if (
106+
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
107+
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
108+
) {
109+
throw RequestError.authRequired();
110+
}
111+
105112
const sessionId = uuidv7();
106113
const input = new Pushable<SDKUserMessage>();
107114

@@ -171,10 +178,18 @@ export class ClaudeAcpAgent implements Agent {
171178
cancelled: false,
172179
};
173180

174-
const availableCommands = await availableSlashCommands(q);
181+
getAvailableSlashCommands(q).then((availableCommands) => {
182+
this.client.sessionUpdate({
183+
sessionId,
184+
update: {
185+
sessionUpdate: "available_commands_update",
186+
availableCommands,
187+
},
188+
});
189+
});
190+
175191
return {
176192
sessionId,
177-
availableCommands,
178193
};
179194
}
180195

@@ -277,7 +292,7 @@ export class ClaudeAcpAgent implements Agent {
277292
}
278293
}
279294

280-
async function availableSlashCommands(query: Query): Promise<AvailableCommand[]> {
295+
async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand[]> {
281296
const UNSUPPORTED_COMMANDS = [
282297
"add-dir",
283298
"agents", // Modal
@@ -313,22 +328,8 @@ async function availableSlashCommands(query: Query): Promise<AvailableCommand[]>
313328
"todos", // Escape Codes
314329
"vim", // Not needed
315330
];
316-
317-
const commands = await Promise.race([
318-
//todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface
319-
(query as any).supportedCommands(),
320-
sleep(10000).then(() => {
321-
if (
322-
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
323-
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
324-
) {
325-
throw RequestError.authRequired();
326-
}
327-
throw new Error(
328-
"Failed to intialize Claude Code.\n\nThis may be caused by incorrect MCP server configuration, try disabling them.",
329-
);
330-
}),
331-
]);
331+
//todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface
332+
const commands = await (query as any).supportedCommands();
332333

333334
return commands
334335
.map((command: { name: string; description: string; argumentHint: string }) => {

src/tests/acp-agent.test.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest";
22
import { spawn, spawnSync } from "child_process";
33
import {
44
Agent,
5+
AvailableCommand,
56
Client,
67
ClientSideConnection,
78
NewSessionResponse,
@@ -47,9 +48,15 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
4748
agent: Agent;
4849
files: Map<string, string> = new Map();
4950
receivedText: string = "";
51+
resolveAvailableCommands: (commands: AvailableCommand[]) => void;
52+
availableCommandsPromise: Promise<AvailableCommand[]>;
5053

5154
constructor(agent: Agent) {
5255
this.agent = agent;
56+
this.resolveAvailableCommands = () => {};
57+
this.availableCommandsPromise = new Promise((resolve) => {
58+
this.resolveAvailableCommands = resolve;
59+
});
5360
}
5461

5562
takeReceivedText() {
@@ -67,11 +74,18 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
6774
async sessionUpdate(params: SessionNotification): Promise<void> {
6875
console.error("RECEIVED", JSON.stringify(params, null, 4));
6976

70-
if (
71-
params.update.sessionUpdate === "agent_message_chunk" &&
72-
params.update.content.type === "text"
73-
) {
74-
this.receivedText += params.update.content.text;
77+
switch (params.update.sessionUpdate) {
78+
case "agent_message_chunk": {
79+
if (params.update.content.type === "text") {
80+
this.receivedText += params.update.content.text;
81+
}
82+
break;
83+
}
84+
case "available_commands_update":
85+
this.resolveAvailableCommands(params.update.availableCommands);
86+
break;
87+
default:
88+
break;
7589
}
7690
}
7791

@@ -140,12 +154,14 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
140154
it("should include available commands", async () => {
141155
const { client, connection, newSessionResponse } = await setupTestSession(__dirname);
142156

143-
expect(newSessionResponse.availableCommands).toContainEqual({
157+
const commands = await client.availableCommandsPromise;
158+
159+
expect(commands).toContainEqual({
144160
name: "quick-math",
145161
description: "10 * 3 = 30 (project)",
146162
input: null,
147163
});
148-
expect(newSessionResponse.availableCommands).toContainEqual({
164+
expect(commands).toContainEqual({
149165
name: "say-hello",
150166
description: "Say hello (project)",
151167
input: { hint: "[name]" },

0 commit comments

Comments
 (0)