mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-31 14:31:05 +00:00
Compare commits
3 Commits
v0.99.1-pr
...
fix/slash-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dd9139965 | ||
|
|
2fd46c866d | ||
|
|
0f78dbbdd0 |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.99.0"
|
||||
".": "0.99.1"
|
||||
}
|
||||
|
||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [0.99.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.99.0...v0.99.1) (2026-10-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add approval id to plan ([#1469](https://github.com/agentclientprotocol/claude-agent-acp/issues/1469)) ([4b4b2c4](https://github.com/agentclientprotocol/claude-agent-acp/commit/4b4b2c4de7c492070f6260cacea5680b2e045a5a))
|
||||
* allow forking sessions into a different directory ([#1483](https://github.com/agentclientprotocol/claude-agent-acp/issues/1483)) ([8bd0bc9](https://github.com/agentclientprotocol/claude-agent-acp/commit/8bd0bc9b03fb4a30e0b1755128baad12a214a58d))
|
||||
* detect "Run /login" login-expired messages as authRequired ([#1481](https://github.com/agentclientprotocol/claude-agent-acp/issues/1481)) ([fccaebc](https://github.com/agentclientprotocol/claude-agent-acp/commit/fccaebcf87e8f37144ec1a64920c0c53997edabe))
|
||||
* mark plugin, bundled and nested Claude skills as skills for AIR ([#1473](https://github.com/agentclientprotocol/claude-agent-acp/issues/1473)) ([52c241e](https://github.com/agentclientprotocol/claude-agent-acp/commit/52c241e71512ec19c4dd79c95c51ce67f04d7aac))
|
||||
* title subagent sessions by their task ([#1479](https://github.com/agentclientprotocol/claude-agent-acp/issues/1479)) ([7dcb629](https://github.com/agentclientprotocol/claude-agent-acp/commit/7dcb62923d38ac24e386b0626fe123334341c445))
|
||||
|
||||
## [0.99.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.98.0...v0.99.0) (2026-10-28)
|
||||
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.99.0",
|
||||
"version": "0.99.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.99.0",
|
||||
"version": "0.99.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "2.1.0",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.99.0",
|
||||
"version": "0.99.1",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
|
||||
@@ -2190,6 +2190,31 @@ const SESSION_ENDED_MESSAGE = "The Claude Agent session has ended. Please start
|
||||
// message and without invoking the model.
|
||||
const LOCAL_ONLY_COMMANDS = new Set(["/context", "/heapdump", "/extra-usage"]);
|
||||
|
||||
// Built-in commands that never use attached context. Clients like Air attach
|
||||
// editor context (resources, extra text chunks) to every prompt, but Claude
|
||||
// Code runs a slash command only when the user message is a single text block.
|
||||
const CONTEXT_FREE_COMMANDS = new Set([
|
||||
...LOCAL_ONLY_COMMANDS,
|
||||
"/compact",
|
||||
"/usage",
|
||||
"/cost",
|
||||
"/stats",
|
||||
"/status",
|
||||
"/model",
|
||||
"/mcp",
|
||||
"/remote-control",
|
||||
]);
|
||||
|
||||
/** Drops the chunks after a context-free built-in command (see
|
||||
* `CONTEXT_FREE_COMMANDS`), keeping the command and its arguments. Returns
|
||||
* `prompt` itself when it isn't such a command. */
|
||||
export function withoutCommandContext(prompt: PromptRequest["prompt"]): PromptRequest["prompt"] {
|
||||
const first = prompt[0];
|
||||
if (prompt.length < 2 || first?.type !== "text") return prompt;
|
||||
const command = first.text.trimStart().split(/\s/, 1)[0];
|
||||
return CONTEXT_FREE_COMMANDS.has(command) ? [first] : prompt;
|
||||
}
|
||||
|
||||
// Commands whose marker-only transcript records are ACP/client-local UI noise,
|
||||
// not model-visible user prompts. Marker-only custom slash skills are not in
|
||||
// this list, so replay can reconstruct them from `<command-name>`/`<command-args>`.
|
||||
@@ -3901,6 +3926,9 @@ export class ClaudeAcpAgent {
|
||||
throw RequestError.internalError(undefined, SESSION_ENDED_MESSAGE);
|
||||
}
|
||||
|
||||
const prompt = withoutCommandContext(params.prompt);
|
||||
if (prompt !== params.prompt) params = { ...params, prompt };
|
||||
|
||||
const remoteControlCommand = parseRemoteControlCommand(params.prompt);
|
||||
if (
|
||||
remoteControlCommand &&
|
||||
|
||||
@@ -45,6 +45,7 @@ export type ClearContextSession<Turn extends ClearContextTurn = ClearContextTurn
|
||||
turnQueue?: Turn[];
|
||||
pendingExitPlanContextReset?: ClearContextReset;
|
||||
contextUsedTokens?: number;
|
||||
contextWindowSize: number;
|
||||
sentUserUuids?: Set<string>;
|
||||
input: { push(message: SDKUserMessage): unknown };
|
||||
};
|
||||
@@ -61,10 +62,13 @@ export type ClearContextCoordinatorHost<
|
||||
): Promise<Session>;
|
||||
applyFastMode(session: Session, enabled: boolean): Promise<void>;
|
||||
applyUltracode(session: Session, enabled: boolean): Promise<void>;
|
||||
/** Publishes the fresh context's mode, config options, and usage: the
|
||||
* context starts empty in a window of `contextWindowSize` tokens. */
|
||||
publishSessionState(
|
||||
sessionId: string,
|
||||
mode: PermissionMode,
|
||||
configOptions: SessionConfigOption[],
|
||||
contextWindowSize: number,
|
||||
): Promise<void>;
|
||||
continuationMessage(sessionId: string, plan: string, promptUuid: string): SDKUserMessage;
|
||||
ensureConsumer(session: Session, sessionId: string): void;
|
||||
@@ -165,7 +169,12 @@ export async function continuePlanInFreshContext<
|
||||
freshSession.contextUsedTokens = 0;
|
||||
|
||||
try {
|
||||
await host.publishSessionState(sessionId, reset.mode, freshSession.configOptions);
|
||||
await host.publishSessionState(
|
||||
sessionId,
|
||||
reset.mode,
|
||||
freshSession.configOptions,
|
||||
freshSession.contextWindowSize,
|
||||
);
|
||||
} catch (error) {
|
||||
host.logError("Failed to publish clear-context session state:", error);
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ export class ExitPlanCoordinator<
|
||||
try {
|
||||
const clearContextHost: ClearContextCoordinatorHost<Session, Turn> = {
|
||||
...this.host,
|
||||
publishSessionState: async (id, mode, configOptions) => {
|
||||
publishSessionState: async (id, mode, configOptions, contextWindowSize) => {
|
||||
await this.host.sessionUpdate({
|
||||
sessionId: id,
|
||||
update: { sessionUpdate: "current_mode_update", currentModeId: mode },
|
||||
@@ -222,6 +222,12 @@ export class ExitPlanCoordinator<
|
||||
sessionId: id,
|
||||
update: { sessionUpdate: "config_option_update", configOptions },
|
||||
});
|
||||
// Otherwise the client shows the old context's usage until the fresh
|
||||
// context first reports its own.
|
||||
await this.host.sessionUpdate({
|
||||
sessionId: id,
|
||||
update: { sessionUpdate: "usage_update", used: 0, size: contextWindowSize },
|
||||
});
|
||||
// The approved plan becomes the session's plan once the fresh
|
||||
// context that carries it out exists. Its ExitPlanMode ended with
|
||||
// an error result, so toAcpNotifications did not publish it.
|
||||
|
||||
@@ -36,6 +36,7 @@ import { ToolCallFieldTracker } from "../tool-calls/field-tracker.js";
|
||||
import {
|
||||
toAcpNotifications,
|
||||
promptToClaude,
|
||||
withoutCommandContext,
|
||||
isLocalCommandMetadata,
|
||||
isReplayHiddenMetaMessage,
|
||||
isSyntheticLoginMessage,
|
||||
@@ -4716,6 +4717,50 @@ describe("escape markdown", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("withoutCommandContext", () => {
|
||||
const link = { type: "resource_link", uri: "file:///a.ts", name: "a.ts" } as const;
|
||||
const resource = {
|
||||
type: "resource",
|
||||
resource: { uri: "file:///a.ts", text: "const a = 1;" },
|
||||
} as const;
|
||||
|
||||
/** `/compact` with attached editor context keeps only the text chunk. */
|
||||
it("drops context after /compact", () => {
|
||||
const text = { type: "text", text: "/compact" } as const;
|
||||
expect(withoutCommandContext([text, link, resource])).toEqual([text]);
|
||||
});
|
||||
|
||||
/** Arguments in the command chunk survive; the attached resource goes. */
|
||||
it("keeps command arguments and drops context", () => {
|
||||
const text = { type: "text", text: "/compact focus on tests" } as const;
|
||||
expect(withoutCommandContext([text, resource])).toEqual([text]);
|
||||
});
|
||||
|
||||
/** An extra text chunk after `/usage` is dropped too. */
|
||||
it("drops trailing text chunks after /usage", () => {
|
||||
const text = { type: "text", text: "/usage" } as const;
|
||||
expect(withoutCommandContext([text, { type: "text", text: "editor context" }])).toEqual([text]);
|
||||
});
|
||||
|
||||
/** Skills and custom commands use attached context, so the prompt is untouched. */
|
||||
it("leaves custom commands unchanged", () => {
|
||||
const prompt = [{ type: "text", text: "/code-review" } as const, resource];
|
||||
expect(withoutCommandContext(prompt)).toBe(prompt);
|
||||
});
|
||||
|
||||
/** A plain text prompt with a resource is untouched. */
|
||||
it("leaves plain prompts unchanged", () => {
|
||||
const prompt = [{ type: "text", text: "explain this" } as const, resource];
|
||||
expect(withoutCommandContext(prompt)).toBe(prompt);
|
||||
});
|
||||
|
||||
/** `/mcp:server:cmd` is an MCP prompt, not the `/mcp` built-in. */
|
||||
it("leaves MCP prompt commands unchanged", () => {
|
||||
const prompt = [{ type: "text", text: "/mcp:server:cmd" } as const, resource];
|
||||
expect(withoutCommandContext(prompt)).toBe(prompt);
|
||||
});
|
||||
});
|
||||
|
||||
describe("prompt conversion", () => {
|
||||
it("should not change built-in slash commands", () => {
|
||||
const message = promptToClaude({
|
||||
|
||||
@@ -28,6 +28,7 @@ function testSession(overrides: Partial<TestSession> = {}): TestSession {
|
||||
},
|
||||
},
|
||||
models: { currentModelId: "default" },
|
||||
contextWindowSize: 200_000,
|
||||
configOptions: [],
|
||||
currentAgent: "default",
|
||||
fastModeEnabled: false,
|
||||
@@ -464,6 +465,7 @@ describe("ExitPlanCoordinator", () => {
|
||||
expect(order).toEqual([
|
||||
"current_mode_update",
|
||||
"config_option_update",
|
||||
"usage_update",
|
||||
...(supportsPlanOperations
|
||||
? [
|
||||
"plan_update " +
|
||||
@@ -476,6 +478,10 @@ describe("ExitPlanCoordinator", () => {
|
||||
: []),
|
||||
"continuation",
|
||||
]);
|
||||
expect(host.sessionUpdate).toHaveBeenCalledWith({
|
||||
sessionId: "public-session",
|
||||
update: { sessionUpdate: "usage_update", used: 0, size: 200_000 },
|
||||
});
|
||||
if (airClient) {
|
||||
expect(host.sessionUpdate).toHaveBeenCalledWith({
|
||||
sessionId: "public-session",
|
||||
|
||||
@@ -635,25 +635,33 @@ describe("/mcp", () => {
|
||||
expect(text()).toBe(formatMcpStatus(SERVERS));
|
||||
});
|
||||
|
||||
it("leaves the text of other /mcp prompts unchanged", async () => {
|
||||
const { agent, sdkQuery, forwarded, text, prompt } = setup();
|
||||
/** A client like Air attaches editor context to `/mcp`; the adapter drops it and still shows the status. */
|
||||
it("shows the status for /mcp with attached context", async () => {
|
||||
const { agent, sdkQuery, forwarded, text } = setup();
|
||||
|
||||
await prompt("/mcp:github:prompt");
|
||||
await prompt("/mcp help");
|
||||
await agent.prompt({
|
||||
sessionId: "test-session",
|
||||
prompt: [
|
||||
{ type: "text", text: "/mcp" },
|
||||
{ type: "text", text: "more" },
|
||||
{ type: "resource", resource: { uri: "file:///a.ts", text: "const a = 1;" } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(forwarded).toEqual(["/mcp"]);
|
||||
expect(text()).toBe(formatMcpStatus(SERVERS));
|
||||
expect(sdkQuery.mcpServerStatus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("leaves the text of other /mcp prompts unchanged", async () => {
|
||||
const { sdkQuery, forwarded, text, prompt } = setup();
|
||||
|
||||
await prompt("/mcp:github:prompt");
|
||||
await prompt("/mcp help");
|
||||
|
||||
// The adapter rewrites an MCP prompt command into the Claude Code form.
|
||||
expect(forwarded).toEqual(["/github:prompt (MCP)", "/mcp help", "/mcp more"]);
|
||||
expect(forwarded).toEqual(["/github:prompt (MCP)", "/mcp help"]);
|
||||
expect(text()).toBe(
|
||||
["/github:prompt (MCP)", "/mcp help", "/mcp more"]
|
||||
.map((command) => cliText(command, SERVERS))
|
||||
.join(""),
|
||||
["/github:prompt (MCP)", "/mcp help"].map((command) => cliText(command, SERVERS)).join(""),
|
||||
);
|
||||
expect(sdkQuery.mcpServerStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user