Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@anthropic-ai/claude-code": "^1.0.100",
"@modelcontextprotocol/sdk": "^1.17.4",
"@types/express": "^5.0.3",
"@zed-industries/agent-client-protocol": "0.2.0-alpha.4",
"@zed-industries/agent-client-protocol": "0.2.0-alpha.6",
"diff": "^8.0.2",
"express": "^5.1.0",
"minimist": "^1.2.8",
Expand Down
41 changes: 21 additions & 20 deletions src/acp-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { v7 as uuidv7 } from "uuid";
import { nodeToWebReadable, nodeToWebWritable, Pushable, sleep, unreachable } from "./utils.js";
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
import { SessionNotification } from "@zed-industries/agent-client-protocol";
import { createMcpServer } from "./mcp-server.js";
import { AddressInfo } from "node:net";
Expand Down Expand Up @@ -102,6 +102,13 @@ export class ClaudeAcpAgent implements Agent {
};
}
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
if (
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
) {
throw RequestError.authRequired();
}

const sessionId = uuidv7();
const input = new Pushable<SDKUserMessage>();

Expand Down Expand Up @@ -171,10 +178,18 @@ export class ClaudeAcpAgent implements Agent {
cancelled: false,
};

const availableCommands = await availableSlashCommands(q);
getAvailableSlashCommands(q).then((availableCommands) => {
this.client.sessionUpdate({
sessionId,
update: {
sessionUpdate: "available_commands_update",
availableCommands,
},
});
});

return {
sessionId,
availableCommands,
};
}

Expand Down Expand Up @@ -277,7 +292,7 @@ export class ClaudeAcpAgent implements Agent {
}
}

async function availableSlashCommands(query: Query): Promise<AvailableCommand[]> {
async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand[]> {
const UNSUPPORTED_COMMANDS = [
"add-dir",
"agents", // Modal
Expand Down Expand Up @@ -313,22 +328,8 @@ async function availableSlashCommands(query: Query): Promise<AvailableCommand[]>
"todos", // Escape Codes
"vim", // Not needed
];

const commands = await Promise.race([
//todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface
(query as any).supportedCommands(),
sleep(10000).then(() => {
if (
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
) {
throw RequestError.authRequired();
}
throw new Error(
"Failed to intialize Claude Code.\n\nThis may be caused by incorrect MCP server configuration, try disabling them.",
);
}),
]);
//todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface
const commands = await (query as any).supportedCommands();

return commands
.map((command: { name: string; description: string; argumentHint: string }) => {
Expand Down
30 changes: 23 additions & 7 deletions src/tests/acp-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { spawn, spawnSync } from "child_process";
import {
Agent,
AvailableCommand,
Client,
ClientSideConnection,
NewSessionResponse,
Expand Down Expand Up @@ -47,9 +48,15 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
agent: Agent;
files: Map<string, string> = new Map();
receivedText: string = "";
resolveAvailableCommands: (commands: AvailableCommand[]) => void;
availableCommandsPromise: Promise<AvailableCommand[]>;

constructor(agent: Agent) {
this.agent = agent;
this.resolveAvailableCommands = () => {};
this.availableCommandsPromise = new Promise((resolve) => {
this.resolveAvailableCommands = resolve;
});
}

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

if (
params.update.sessionUpdate === "agent_message_chunk" &&
params.update.content.type === "text"
) {
this.receivedText += params.update.content.text;
switch (params.update.sessionUpdate) {
case "agent_message_chunk": {
if (params.update.content.type === "text") {
this.receivedText += params.update.content.text;
}
break;
}
case "available_commands_update":
this.resolveAvailableCommands(params.update.availableCommands);
break;
default:
break;
}
}

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

expect(newSessionResponse.availableCommands).toContainEqual({
const commands = await client.availableCommandsPromise;

expect(commands).toContainEqual({
name: "quick-math",
description: "10 * 3 = 30 (project)",
input: null,
});
expect(newSessionResponse.availableCommands).toContainEqual({
expect(commands).toContainEqual({
name: "say-hello",
description: "Say hello (project)",
input: { hint: "[name]" },
Expand Down