diff --git a/src/acp-agent.ts b/src/acp-agent.ts index 42a3f2eb..274edd15 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -259,14 +259,19 @@ export class ClaudeAcpAgent implements Agent { throw RequestError.authRequired(); } - return await this.createSession(params, { + const response = await this.createSession(params, { // Revisit these meta values once we support resume resume: (params._meta as NewSessionMeta | undefined)?.claudeCode?.options?.resume, }); + // Needs to happen after we return the session + setTimeout(() => { + this.sendAvailableCommandsUpdate(response.sessionId); + }, 0); + return response; } async unstable_forkSession(params: ForkSessionRequest): Promise { - return await this.createSession( + const response = await this.createSession( { cwd: params.cwd, mcpServers: params.mcpServers ?? [], @@ -277,6 +282,11 @@ export class ClaudeAcpAgent implements Agent { forkSession: true, }, ); + // Needs to happen after we return the session + setTimeout(() => { + this.sendAvailableCommandsUpdate(response.sessionId); + }, 0); + return response; } async unstable_resumeSession(params: ResumeSessionRequest): Promise { @@ -290,7 +300,10 @@ export class ClaudeAcpAgent implements Agent { resume: params.sessionId, }, ); - + // Needs to happen after we return the session + setTimeout(() => { + this.sendAvailableCommandsUpdate(response.sessionId); + }, 0); return response; } @@ -353,6 +366,11 @@ export class ClaudeAcpAgent implements Agent { await this.replaySessionHistory(params.sessionId, filePath); + // Send available commands after replay so it doesn't interleave with history + setTimeout(() => { + this.sendAvailableCommandsUpdate(params.sessionId); + }, 0); + return { modes: response.modes, models: response.models, @@ -951,6 +969,19 @@ export class ClaudeAcpAgent implements Agent { }; } + private async sendAvailableCommandsUpdate(sessionId: string): Promise { + const session = this.sessions[sessionId]; + if (!session) return; + const commands = await session.query.supportedCommands(); + await this.client.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "available_commands_update", + availableCommands: getAvailableSlashCommands(commands), + }, + }); + } + private async createSession( params: NewSessionRequest, creationOpts: { resume?: string; forkSession?: boolean } = {}, @@ -1170,17 +1201,6 @@ export class ClaudeAcpAgent implements Agent { const models = await getAvailableModels(q, initializationResult.models, settingsManager); - // Needs to happen after we return the session - setTimeout(() => { - this.client.sessionUpdate({ - sessionId, - update: { - sessionUpdate: "available_commands_update", - availableCommands: getAvailableSlashCommands(initializationResult.commands), - }, - }); - }, 0); - const availableModes = [ { id: "default", diff --git a/src/tests/load-session.test.ts b/src/tests/load-session.test.ts index 2c9ef47c..7523319a 100644 --- a/src/tests/load-session.test.ts +++ b/src/tests/load-session.test.ts @@ -258,6 +258,48 @@ describe("loadSession", () => { }); }); + it("sends available_commands_update after history replay, not during", async () => { + const cwd = "/Users/test/project"; + const sessionId = "session-commands"; + + // Mock createSession to populate the sessions map with a mock query + createSessionSpy.mockImplementationOnce(async () => { + (agent as unknown as { sessions: Record }).sessions[sessionId] = { + query: { + supportedCommands: async () => [{ name: "help", description: "Get help" }], + }, + }; + return { modes: null, models: null }; + }); + + writeSessionFile(cwd, sessionId, [ + { + type: "user", + sessionId, + message: { role: "user", content: "Hello" }, + }, + { + type: "assistant", + sessionId, + message: { role: "assistant", content: [{ type: "text", text: "Hi" }] }, + }, + ]); + + await agent.loadSession({ cwd, sessionId, mcpServers: [] }); + + // Flush the setTimeout + async work inside sendAvailableCommandsUpdate + await new Promise((resolve) => setTimeout(resolve, 10)); + + const updateTypes = sessionUpdates.map((n) => n.update.sessionUpdate); + + // available_commands_update should come AFTER all replay notifications + expect(updateTypes).toEqual([ + "user_message_chunk", + "agent_message_chunk", + "available_commands_update", + ]); + }); + it("handles Windows-style paths", async () => { const cwd = "C:\\Users\\test\\project"; const sessionId = "win-session";