@agentclientprotocol/claude-agent-acp 0.85.2-preview.1 → 0.85.2-preview.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +1 -0
  2. package/dist/acp-agent.d.ts +113 -27
  3. package/dist/acp-agent.d.ts.map +1 -1
  4. package/dist/acp-agent.js +477 -175
  5. package/dist/air-extension.d.ts +2 -0
  6. package/dist/air-extension.d.ts.map +1 -1
  7. package/dist/air-extension.js +2 -0
  8. package/dist/diff.d.ts +42 -5
  9. package/dist/diff.d.ts.map +1 -1
  10. package/dist/diff.js +103 -26
  11. package/dist/mcp-command.d.ts +51 -0
  12. package/dist/mcp-command.d.ts.map +1 -0
  13. package/dist/mcp-command.js +321 -0
  14. package/dist/resumed-session.d.ts +16 -6
  15. package/dist/resumed-session.d.ts.map +1 -1
  16. package/dist/resumed-session.js +91 -25
  17. package/dist/session-mode.d.ts +14 -0
  18. package/dist/session-mode.d.ts.map +1 -1
  19. package/dist/session-mode.js +22 -0
  20. package/dist/tool-calls/client-capabilities.d.ts +16 -2
  21. package/dist/tool-calls/client-capabilities.d.ts.map +1 -1
  22. package/dist/tool-calls/client-capabilities.js +11 -3
  23. package/dist/tool-calls/reporters/file-edit.d.ts +1 -1
  24. package/dist/tool-calls/reporters/file-edit.d.ts.map +1 -1
  25. package/dist/tool-calls/reporters/file-edit.js +18 -14
  26. package/dist/turn-events.d.ts +71 -0
  27. package/dist/turn-events.d.ts.map +1 -0
  28. package/dist/turn-events.js +1 -0
  29. package/dist/usage-markdown.d.ts +2 -0
  30. package/dist/usage-markdown.d.ts.map +1 -1
  31. package/dist/usage-markdown.js +2 -1
  32. package/dist/utils.d.ts +12 -0
  33. package/dist/utils.d.ts.map +1 -1
  34. package/dist/utils.js +28 -0
  35. package/dist/v2/agent.d.ts.map +1 -1
  36. package/dist/v2/agent.js +27 -5
  37. package/dist/v2/prompt.d.ts +47 -0
  38. package/dist/v2/prompt.d.ts.map +1 -0
  39. package/dist/v2/prompt.js +153 -0
  40. package/dist/v2/session-update.d.ts.map +1 -1
  41. package/dist/v2/session-update.js +14 -0
  42. package/dist/v2/tool-call.d.ts +32 -0
  43. package/dist/v2/tool-call.d.ts.map +1 -0
  44. package/dist/v2/tool-call.js +42 -0
  45. package/package.json +2 -2
package/dist/acp-agent.js CHANGED
@@ -4,7 +4,7 @@ import { GOAL_ACTIONS, GOAL_CONTROL_METHOD, GOAL_EXTENSION_VERSION, goalUpdateFr
4
4
  import { sanitizeTitle, SessionTitles } from "./session-titles.js";
5
5
  import { AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, asSdkSessionNotification, clientSupportsSubagents, } from "./acp-subagents.js";
6
6
  import { isNativeSubagentControlTool, isNativeSubagentControlUpdate, NativeSubagentRuntime, nativeSubagentState, resumedNativeSubagentId, sendMessageResumePrompt, } from "./native-subagents.js";
7
- import { AIR_ASYNC_TASKS_CAPABILITY, AIR_DIFF_PATCH_CAPABILITY, AIR_PLAN_FILE_CAPABILITY, AIR_GOAL_KEY, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, clientSupportsAirCapability, withAirMeta, } from "./air-extension.js";
7
+ import { AIR_ASYNC_TASKS_CAPABILITY, AIR_DIFF_PATCH_CAPABILITY, AIR_PLAN_FILE_CAPABILITY, AIR_GOAL_KEY, AIR_KIND_KEY, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, AIR_SKILL_PATH_KEY, clientSupportsAirCapability, withAirMeta, } from "./air-extension.js";
8
8
  import { AsyncTaskRuntime, backgroundBashTaskFromToolResult, clientSupportsAsyncTasks, } from "./async-tasks.js";
9
9
  import { AUTH_STATUS_PROBE_TIMEOUT_MS, AUTH_STATUS_UPDATE_METHOD, authStatusCapability, fromAccountInfo, fromCliStatus, gatewayAuthStatus, mergeAuthStatus, notLoggedInAuthStatus, sameAuthStatus, } from "./auth-status.js";
10
10
  import { execFile } from "node:child_process";
@@ -16,9 +16,9 @@ import packageJson from "../package.json" with { type: "json" };
16
16
  import { applyAskElicitationResponse, askUserQuestionsToCreateRequest, createElicitationResponseToElicitResult, extractAskUserQuestions, extractRefusalFallbackPrompt, mcpElicitationToCreateRequest, REFUSAL_FALLBACK_DIALOG_KIND, refusalFallbackResultFromResponse, refusalFallbackToCreateRequest, } from "./elicitation.js";
17
17
  import { forkSession } from "./fork-session.js";
18
18
  import { subagentHistory } from "./subagent-history.js";
19
- import { readResumedModel, readResumedSession, } from "./resumed-session.js";
19
+ import { readResumedSession, readResumedTail, } from "./resumed-session.js";
20
20
  import { SessionTiming } from "./session-timing.js";
21
- import { ALLOW_BYPASS, resolvePermissionMode } from "./permissions/modes.js";
21
+ import { ALLOW_BYPASS } from "./permissions/modes.js";
22
22
  import { normalizeDurablePermissionChangeSet } from "./permissions/normalization.js";
23
23
  import { buildClaudePermissionOptions } from "./permissions/options.js";
24
24
  import { buildClaudePermissionPresentation } from "./permissions/presentation.js";
@@ -36,11 +36,13 @@ import { ChangedMetaFilter } from "./tool-calls/changed-meta-filter.js";
36
36
  import { ToolCallFieldTracker } from "./tool-calls/field-tracker.js";
37
37
  import { ClientCapabilities as ToolCallClientCapabilities } from "./tool-calls/client-capabilities.js";
38
38
  import { AcpToolCallRenderer } from "./tool-calls/renderer.js";
39
- import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
39
+ import { resolveSkillPath } from "./tool-calls/reporters/interaction.js";
40
+ import { nodeToWebReadable, nodeToWebWritable, Pushable, raceTimeoutAndAbort, unreachable, } from "./utils.js";
40
41
  import { acceptedPlanToolResult, ExitPlanCoordinator, isExitPlanInterruptionResult, observeExitPlanToolResults, } from "./exit-plan.js";
41
42
  import { parseToolResultMeta } from "./tool-result-meta.js";
42
43
  import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "./usage-markdown.js";
43
- import { MODE_CONFIG_ID, SessionModeManager } from "./session-mode.js";
44
+ import { MCP_AVAILABLE_COMMAND, parseMcpCommand, runMcpCommand } from "./mcp-command.js";
45
+ import { MODE_CONFIG_ID, resolveInitialPermissionMode, SessionModeManager, } from "./session-mode.js";
44
46
  import { applyAvailableModelsAllowlist, buildModelConfigOption, getAvailableModels, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
45
47
  import { buildEffortConfigOption, EFFORT_CONFIG_ID, effortFlagSettings, mergeEffortSettings, settingsEffortForModel, } from "./session-effort.js";
46
48
  export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
@@ -105,32 +107,18 @@ function settleLiveBackgroundTask(session, taskId) {
105
107
  async function structuredUsageMarkdown(query, signal, logger) {
106
108
  if (signal.aborted)
107
109
  return null;
108
- let timeout;
109
- let onAbort;
110
110
  try {
111
- const response = await Promise.race([
112
- // Keeping the deliberately unstable method name visible makes an SDK
113
- // upgrade fail at compile time if Anthropic removes or renames it.
114
- query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(),
115
- new Promise((resolve) => {
116
- timeout = setTimeout(() => resolve(null), STRUCTURED_USAGE_TIMEOUT_MS);
117
- timeout.unref?.();
118
- }),
119
- new Promise((resolve) => {
120
- onAbort = () => resolve(null);
121
- if (signal.aborted)
122
- onAbort();
123
- else
124
- signal.addEventListener("abort", onAbort, { once: true });
125
- }),
126
- ]);
127
- if (response === null) {
128
- if (!signal.aborted) {
111
+ const outcome = await raceTimeoutAndAbort(
112
+ // Keeping the deliberately unstable method name visible makes an SDK
113
+ // upgrade fail at compile time if Anthropic removes or renames it.
114
+ query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(), STRUCTURED_USAGE_TIMEOUT_MS, signal);
115
+ if (outcome.type !== "done") {
116
+ if (outcome.type === "timeout") {
129
117
  logger.error("Structured /usage timed out; preserving Claude Code output");
130
118
  }
131
119
  return null;
132
120
  }
133
- const usage = parseUsageResponse(response);
121
+ const usage = parseUsageResponse(outcome.value);
134
122
  if (!usage) {
135
123
  logger.error("Structured /usage returned an incompatible response; preserving Claude Code output");
136
124
  return null;
@@ -141,12 +129,6 @@ async function structuredUsageMarkdown(query, signal, logger) {
141
129
  logger.error(`Structured /usage failed; preserving Claude Code output: ${error}`);
142
130
  return null;
143
131
  }
144
- finally {
145
- if (timeout)
146
- clearTimeout(timeout);
147
- if (onAbort)
148
- signal.removeEventListener("abort", onAbort);
149
- }
150
132
  }
151
133
  /** Claude Code keeps the OAuth callback listener open in the background after
152
134
  * `mcpAuthenticate` returns the authorization URL. The SDK does not expose
@@ -680,6 +662,7 @@ const REPLAY_HIDDEN_COMMANDS = new Set([
680
662
  ...LOCAL_ONLY_COMMANDS,
681
663
  "/compact",
682
664
  "/model",
665
+ "/mcp",
683
666
  "/status",
684
667
  "/usage",
685
668
  ]);
@@ -1017,12 +1000,19 @@ const mcpAuthentications = new WeakMap();
1017
1000
  * authentication. This runs after session creation has returned so an
1018
1001
  * interactive browser flow never delays `session/new`. */
1019
1002
  function startMcpAuthentication(host, sessionId, mcpServers) {
1020
- if (!host.clientCapabilities?.elicitation?.url || mcpServers.length === 0)
1003
+ if (mcpServers.length === 0)
1004
+ return;
1005
+ startMcpServerAuthentication(host, sessionId, new Set(mcpServers.map((server) => server.name)));
1006
+ }
1007
+ /** Start OAuth in the background for the servers in `requestedServers` that
1008
+ * Claude reports as needing authentication. Startup and `/mcp` use it. It
1009
+ * does nothing while another OAuth run of the session is in progress. */
1010
+ function startMcpServerAuthentication(host, sessionId, requestedServers) {
1011
+ if (!host.clientCapabilities?.elicitation?.url)
1021
1012
  return;
1022
1013
  const session = host.sessions[sessionId];
1023
1014
  if (!session || mcpAuthentications.has(session))
1024
1015
  return;
1025
- const requestedServers = new Set(mcpServers.map((server) => server.name));
1026
1016
  const authentication = authenticateMcpServers(host, sessionId, session.query, requestedServers)
1027
1017
  .catch((error) => {
1028
1018
  if (!session.abortController.signal.aborted) {
@@ -1161,7 +1151,14 @@ export class ClaudeAcpAgent {
1161
1151
  * return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
1162
1152
  * tests can shrink it. */
1163
1153
  forceCancelGraceMs = DEFAULT_FORCE_CANCEL_GRACE_MS;
1164
- constructor(client, logger) {
1154
+ /**
1155
+ * The connection serves ACP v2 (`src/v2/`). The agent still speaks v1
1156
+ * types, except for the tool call content that v2 needs and v1 cannot
1157
+ * express: see {@link ToolCallClientCapabilities.v2}.
1158
+ */
1159
+ v2;
1160
+ constructor(client, logger, options = {}) {
1161
+ this.v2 = options.v2 ?? false;
1165
1162
  this.sessions = {};
1166
1163
  this.client = new ChangedMetaClient(client, () => this.toolCallCapabilities.air.client);
1167
1164
  this.logger = logger ?? console;
@@ -1218,7 +1215,9 @@ export class ClaudeAcpAgent {
1218
1215
  }
1219
1216
  async initialize(request) {
1220
1217
  this.clientCapabilities = request.clientCapabilities;
1221
- this.toolCallCapabilities = ToolCallClientCapabilities.from(request.clientCapabilities);
1218
+ this.toolCallCapabilities = ToolCallClientCapabilities.from(request.clientCapabilities, {
1219
+ v2: this.v2,
1220
+ });
1222
1221
  // Learn the auth identity in the background: `initialize` never waits on
1223
1222
  // the CLI probe, and no snapshot rides in its response. When the probe
1224
1223
  // lands it calls `setAuthStatus`, which pushes `_auth/status_update` — the
@@ -1402,6 +1401,56 @@ export class ClaudeAcpAgent {
1402
1401
  }, 0);
1403
1402
  return response;
1404
1403
  }
1404
+ /**
1405
+ * Start the CLI of a loaded session, and replay the history while the CLI
1406
+ * starts. The CLI start takes most of the load time, and the replay needs
1407
+ * only the transcript and the {@link ReplayState}. An invalid `cwd` fails
1408
+ * before the first update. A failed CLI start stops the replay.
1409
+ */
1410
+ async createSessionWhileReplaying(params, timing) {
1411
+ await this.validateCwd(params.cwd);
1412
+ const pending = {
1413
+ state: {
1414
+ cwd: params.cwd,
1415
+ taskState: new Map(),
1416
+ forwardSubagentText: this.forwardsSubagentText(params._meta),
1417
+ messageIdToUuid: new Map(),
1418
+ sessionFailureState: createSessionFailureState(),
1419
+ },
1420
+ stopped: false,
1421
+ };
1422
+ const transcript = readResumedSession(params.sessionId, this.logger);
1423
+ const replay = transcript
1424
+ .then(({ messages }) => this.replaySessionHistory(params.sessionId, messages, pending))
1425
+ .then(() => ({ ok: true }), (error) => ({ ok: false, error }));
1426
+ let response;
1427
+ try {
1428
+ response = await this.createSession({
1429
+ cwd: params.cwd,
1430
+ mcpServers: params.mcpServers ?? [],
1431
+ additionalDirectories: params.additionalDirectories,
1432
+ _meta: params._meta,
1433
+ }, {
1434
+ resume: params.sessionId,
1435
+ resumedModelHint: transcript.then(({ model }) => model),
1436
+ replayState: pending.state,
1437
+ });
1438
+ }
1439
+ catch (error) {
1440
+ pending.stopped = true;
1441
+ await replay;
1442
+ throw error;
1443
+ }
1444
+ timing.phase("session-ready");
1445
+ const replayed = await replay;
1446
+ if (!replayed.ok)
1447
+ throw replayed.error;
1448
+ return {
1449
+ sessionId: response.sessionId,
1450
+ modes: response.modes,
1451
+ configOptions: response.configOptions,
1452
+ };
1453
+ }
1405
1454
  async unstable_forkSession(params) {
1406
1455
  if (this.providerUpdate)
1407
1456
  await this.providerUpdate;
@@ -1426,10 +1475,16 @@ export class ClaudeAcpAgent {
1426
1475
  const timing = new SessionTiming(this.logger, "load", params.sessionId);
1427
1476
  if (this.providerUpdate)
1428
1477
  await this.providerUpdate;
1429
- const resumedSession = await readResumedSession(params.sessionId, this.logger);
1430
- const result = await this.getOrCreateSession(params, resumedSession);
1431
- timing.phase("session-ready");
1432
- await this.replaySessionHistory(params.sessionId, resumedSession.messages);
1478
+ let result;
1479
+ if (this.sessions[params.sessionId]) {
1480
+ const resumedSession = await readResumedSession(params.sessionId, this.logger);
1481
+ result = await this.getOrCreateSession(params, resumedSession);
1482
+ timing.phase("session-ready");
1483
+ await this.replaySessionHistory(params.sessionId, resumedSession.messages);
1484
+ }
1485
+ else {
1486
+ result = await this.createSessionWhileReplaying(params, timing);
1487
+ }
1433
1488
  timing.phase("replay");
1434
1489
  // Send available commands after replay so it doesn't interleave with history
1435
1490
  setTimeout(() => {
@@ -1812,7 +1867,72 @@ export class ClaudeAcpAgent {
1812
1867
  this.setAuthStatus(notLoggedInAuthStatus());
1813
1868
  }
1814
1869
  }
1870
+ /** The structured replacement for a prompt that is exactly `/usage` or a
1871
+ * `/mcp` command. Claude Code runs the command, and the replacement takes
1872
+ * the place of its text. Claude Code refuses a `/mcp` reconnect, enable,
1873
+ * or disable in SDK mode. The `/mcp` producer runs them through the SDK
1874
+ * control API when the text arrives, so the action stays in the turn. */
1875
+ localCommandMarkdown(params) {
1876
+ const text = params.prompt.length === 1 && params.prompt[0]?.type === "text"
1877
+ ? params.prompt[0].text
1878
+ : undefined;
1879
+ if (text === undefined)
1880
+ return undefined;
1881
+ if (isUsageCommandText(text)) {
1882
+ return {
1883
+ startsAtActivation: true,
1884
+ produce: (query, signal) => structuredUsageMarkdown(query, signal, this.logger),
1885
+ };
1886
+ }
1887
+ const command = parseMcpCommand(text);
1888
+ if (!command)
1889
+ return undefined;
1890
+ let reconnected = [];
1891
+ return {
1892
+ startsAtActivation: false,
1893
+ produce: async (query, signal, originalOutput) => {
1894
+ const outcome = await runMcpCommand(query, command, originalOutput, signal, (message) => this.logger.error(message));
1895
+ reconnected = outcome.reconnected;
1896
+ return outcome.markdown;
1897
+ },
1898
+ // A reconnect starts OAuth for the ACP servers that it tried, as the
1899
+ // session start does. A plain `/mcp` and a cancelled turn start none.
1900
+ afterDelivery: () => {
1901
+ const session = this.sessions[params.sessionId];
1902
+ if (!session || session.cancelled)
1903
+ return;
1904
+ const acpServers = new Set((session.creationParams?.mcpServers ?? []).map((server) => server.name));
1905
+ const servers = new Set(reconnected.filter((server) => acpServers.has(server)));
1906
+ if (servers.size > 0)
1907
+ startMcpServerAuthentication(this, params.sessionId, servers);
1908
+ },
1909
+ };
1910
+ }
1911
+ /**
1912
+ * Serves an ACP v1 `session/prompt`, which answers when the turn ends. The
1913
+ * outcome of a turn is field for field a v1 prompt response.
1914
+ */
1815
1915
  async prompt(params) {
1916
+ let events;
1917
+ const outcome = new Promise((resolve, reject) => {
1918
+ events = {
1919
+ inserted() { },
1920
+ awaitingUser() { },
1921
+ resumed() { },
1922
+ ended: resolve,
1923
+ failed: reject,
1924
+ };
1925
+ });
1926
+ await this.startTurn(params, events);
1927
+ return outcome;
1928
+ }
1929
+ /**
1930
+ * Starts a turn for a prompt. Resolves once the prompt is queued for Claude
1931
+ * Code, and rejects only when the prompt is refused before that. From then
1932
+ * on, `events` reports how the turn goes (see {@link TurnEvents}), possibly
1933
+ * before this resolves.
1934
+ */
1935
+ async startTurn(params, events) {
1816
1936
  if (this.providerUpdate)
1817
1937
  await this.providerUpdate;
1818
1938
  let session = this.sessions[params.sessionId];
@@ -1854,45 +1974,41 @@ export class ClaudeAcpAgent {
1854
1974
  const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
1855
1975
  const isLocalOnlyCommand = firstText.startsWith("/") && LOCAL_ONLY_COMMANDS.has(firstText.split(" ", 1)[0]);
1856
1976
  const fileChangeReport = session.fileChangeReporter?.request(params._meta);
1857
- const isUsageCommand = params.prompt.length === 1 &&
1858
- params.prompt[0]?.type === "text" &&
1859
- isUsageCommandText(params.prompt[0].text);
1860
- const usageMarkdownAbort = isUsageCommand ? new AbortController() : undefined;
1977
+ const localCommand = this.localCommandMarkdown(params);
1978
+ const localCommandAbort = localCommand ? new AbortController() : undefined;
1861
1979
  session.titles.onPrompt(params.prompt);
1862
- // Each prompt is a Turn whose deferred the persistent consumer settles once
1863
- // the turn's outcome is known. `prompt()` owns no loop: it enqueues the
1864
- // turn, pushes the user message onto the streaming input, makes sure the
1865
- // consumer is running, and awaits the deferred.
1980
+ // Each prompt is a Turn that the persistent consumer settles once the
1981
+ // turn's outcome is known. `startTurn()` owns no loop: it enqueues the
1982
+ // turn, pushes the user message onto the streaming input, and makes sure
1983
+ // the consumer is running.
1984
+ let completeTurn;
1866
1985
  const turn = {
1867
1986
  promptUuid,
1987
+ events,
1868
1988
  isLocalOnlyCommand,
1869
- ...(isUsageCommand ? { isUsageCommand: true } : {}),
1870
- ...(usageMarkdownAbort ? { usageMarkdownAbort } : {}),
1989
+ ...(localCommand ? { localCommand } : {}),
1990
+ ...(localCommandAbort ? { localCommandAbort } : {}),
1871
1991
  ...(fileChangeReport ? { fileChangeReport } : {}),
1872
1992
  settled: false,
1873
- resolve: () => { },
1874
- reject: () => { },
1875
- };
1876
- let completeTurn;
1877
- turn.completion = new Promise((resolve) => {
1878
- completeTurn = resolve;
1879
- });
1880
- const response = new Promise((resolve, reject) => {
1881
- turn.resolve = (result) => {
1882
- resolve(result);
1993
+ completion: new Promise((resolve) => {
1994
+ completeTurn = resolve;
1995
+ }),
1996
+ resolve: (outcome) => {
1997
+ events.ended(outcome);
1883
1998
  completeTurn();
1884
- };
1885
- turn.reject = (error) => {
1886
- reject(error);
1999
+ },
2000
+ reject: (error, title) => {
2001
+ events.failed(error, title);
1887
2002
  completeTurn();
1888
- };
1889
- });
2003
+ },
2004
+ };
1890
2005
  session.turnQueue ??= [];
1891
2006
  session.turnQueue.push(turn);
1892
2007
  session.input.push(userMessage);
1893
2008
  this.ensureConsumer(session, params.sessionId);
1894
- await this.publishGoalFromPrompt(params.sessionId, firstText, promptUuid);
1895
- return response;
2009
+ // The prompt is queued, so its turn goes ahead even if the client misses
2010
+ // the goal it sets; the turn's events report the outcome.
2011
+ await this.publishGoalFromPrompt(params.sessionId, firstText, promptUuid).catch((error) => this.logger.error(`Session ${params.sessionId}: failed to publish the prompt's goal:`, error));
1896
2012
  }
1897
2013
  /** `--hide-claude-auth` applies only to the CLI's own login. A provider
1898
2014
  * override (`providers/set` or gateway `authenticate`) routes traffic away
@@ -1993,7 +2109,10 @@ export class ClaudeAcpAgent {
1993
2109
  try {
1994
2110
  // `resume` names the Claude session, which shares the ACP session id, so
1995
2111
  // the new query continues the same conversation under the same id.
1996
- await this.createSession(creationParams, { resume: sessionId });
2112
+ await this.createSession(creationParams, {
2113
+ resume: sessionId,
2114
+ permissionMode: session.modes.currentModeId,
2115
+ });
1997
2116
  }
1998
2117
  catch (error) {
1999
2118
  if (error instanceof RequestError && error.code === RequestError.resourceNotFound().code) {
@@ -2003,7 +2122,10 @@ export class ClaudeAcpAgent {
2003
2122
  // better answer than a session the client can never use again.
2004
2123
  this.logger.log(`session ${sessionId} was never persisted; starting a fresh query under the same id`);
2005
2124
  try {
2006
- await this.createSession(creationParams, { reuseSessionId: sessionId });
2125
+ await this.createSession(creationParams, {
2126
+ reuseSessionId: sessionId,
2127
+ permissionMode: session.modes.currentModeId,
2128
+ });
2007
2129
  return;
2008
2130
  }
2009
2131
  catch (fallbackError) {
@@ -2465,11 +2587,11 @@ export class ClaudeAcpAgent {
2465
2587
  };
2466
2588
  };
2467
2589
  const internalErrorForClient = (data, rawDetail) => RequestError.internalError(data, supportsAirSessionFailures(this.clientCapabilities) ? undefined : rawDetail);
2468
- const ensureUsageMarkdown = (turn) => {
2469
- if (!turn.isUsageCommand || !turn.usageMarkdownAbort)
2590
+ const ensureLocalCommandMarkdown = (turn, originalOutput = "") => {
2591
+ if (!turn.localCommand || !turn.localCommandAbort)
2470
2592
  return undefined;
2471
- turn.usageMarkdown ??= structuredUsageMarkdown(session.query, turn.usageMarkdownAbort.signal, this.logger);
2472
- return turn.usageMarkdown;
2593
+ turn.localCommandMarkdown ??= turn.localCommand.produce(session.query, turn.localCommandAbort.signal, originalOutput);
2594
+ return turn.localCommandMarkdown;
2473
2595
  };
2474
2596
  const resetTurnScratch = () => {
2475
2597
  lastAssistantTotalUsage = null;
@@ -2512,7 +2634,8 @@ export class ClaudeAcpAgent {
2512
2634
  session.activeTurn = turn;
2513
2635
  session.cancelled = false;
2514
2636
  compaction.resume();
2515
- ensureUsageMarkdown(turn);
2637
+ if (turn.localCommand?.startsAtActivation)
2638
+ ensureLocalCommandMarkdown(turn);
2516
2639
  session.pendingOrphanResults = 0;
2517
2640
  session.orphanCommands?.clear();
2518
2641
  // Two-phase sweep of registry entries the level signal ended (see
@@ -2540,6 +2663,22 @@ export class ClaudeAcpAgent {
2540
2663
  }
2541
2664
  }
2542
2665
  resetTurnScratch();
2666
+ // Without a command_lifecycle "started" frame before it (see that
2667
+ // handler), activation is the first sign that Claude Code took the
2668
+ // prompt in: its echo, or for an echo-less command, its result.
2669
+ reportInserted(turn);
2670
+ // A request can already be open: Claude Code asks for a queued prompt
2671
+ // before the consumer reaches its echo.
2672
+ this.syncAwaitingUser(session);
2673
+ };
2674
+ /** Report that Claude Code took the turn's prompt in, once. A turn can be
2675
+ * settled by now: an echo hand-off awaits settling the previous turn, and
2676
+ * a cancel in that window settles the queued one. */
2677
+ const reportInserted = (turn) => {
2678
+ if (turn.insertedReported || turn.settled)
2679
+ return;
2680
+ turn.insertedReported = true;
2681
+ turn.events.inserted(turn.promptUuid);
2543
2682
  };
2544
2683
  /** Ensure there is an active turn before a user-turn result that carries no
2545
2684
  * echo to activate it, by promoting the queue head. Most turns are
@@ -2699,27 +2838,28 @@ export class ClaudeAcpAgent {
2699
2838
  * the autonomous stretch-close guard. */
2700
2839
  const firstUnsettledQueuedTurn = () => (session.turnQueue ?? []).find((t) => !t.settled);
2701
2840
  /** Claim the structured replacement for the turn currently producing a
2702
- * local-command output. Undefined means this is not a structured usage
2703
- * turn (or its request failed), null means another SDK message shape
2704
- * already delivered it, and string is the one replacement to publish. */
2705
- const takeUsageMarkdown = async (originalOutput) => {
2841
+ * local-command output. Undefined means this is not a structured
2842
+ * local-command turn (or its request failed), null means another SDK
2843
+ * message shape already delivered it, and string is the one replacement
2844
+ * to publish. */
2845
+ const takeLocalCommandMarkdown = async (originalOutput) => {
2706
2846
  const turn = session.activeTurn ?? firstUnsettledQueuedTurn();
2707
2847
  if (!turn)
2708
2848
  return undefined;
2709
- const usageMarkdown = ensureUsageMarkdown(turn);
2710
- if (!usageMarkdown)
2849
+ const pending = ensureLocalCommandMarkdown(turn, originalOutput);
2850
+ if (!pending)
2711
2851
  return undefined;
2712
- const markdown = await usageMarkdown;
2852
+ const markdown = await pending;
2713
2853
  if (markdown === null)
2714
2854
  return undefined;
2715
- if (turn.usageMarkdownDelivered) {
2855
+ if (turn.localCommandDelivered) {
2716
2856
  // Different SDK message shapes can mirror the same local-command
2717
2857
  // output. Suppress an exact mirror, but let a later, distinct frame
2718
2858
  // (for example an interruption diagnostic) follow the normal path.
2719
- return turn.usageOriginalOutput === originalOutput ? null : undefined;
2859
+ return turn.localCommandOriginalOutput === originalOutput ? null : undefined;
2720
2860
  }
2721
- turn.usageMarkdownDelivered = true;
2722
- turn.usageOriginalOutput = originalOutput;
2861
+ turn.localCommandDelivered = true;
2862
+ turn.localCommandOriginalOutput = originalOutput;
2723
2863
  return markdown;
2724
2864
  };
2725
2865
  /** Registers a live background task (see `liveBackgroundTasks`). A
@@ -2872,7 +3012,9 @@ export class ClaudeAcpAgent {
2872
3012
  // Captured before the settled flip below (isHeldOpen tests !settled).
2873
3013
  const wasHeld = isHeldOpen(turn);
2874
3014
  turn.settled = true;
2875
- turn.usageMarkdownAbort?.abort();
3015
+ turn.localCommandAbort?.abort();
3016
+ if (turn.localCommandDelivered)
3017
+ turn.localCommand?.afterDelivery?.();
2876
3018
  disarmForceCancel(session);
2877
3019
  session.turnQueue = (session.turnQueue ?? []).filter((t) => t !== turn);
2878
3020
  session.activeTurn = null;
@@ -2896,7 +3038,7 @@ export class ClaudeAcpAgent {
2896
3038
  };
2897
3039
  /** Reject the active turn (auth required, error result, …) without tearing
2898
3040
  * down the consumer: the stream continues to idle and later turns proceed. */
2899
- const failActive = (error) => {
3041
+ const failActive = (error, title) => {
2900
3042
  disarmForceCancel(session);
2901
3043
  const turn = session.activeTurn;
2902
3044
  if (!turn || turn.settled) {
@@ -2913,7 +3055,7 @@ export class ClaudeAcpAgent {
2913
3055
  // start the next stretch clean, or its stale delivery record would
2914
3056
  // suppress the next turn's issue-#453 result-text fallback.
2915
3057
  session.emittedAssistantText = false;
2916
- turn.reject(error);
3058
+ turn.reject(error, title);
2917
3059
  };
2918
3060
  /** Complete a negotiated terminal failure on the prompt response itself,
2919
3061
  * which is the canonical AIR carrier. Legacy clients keep the historical
@@ -2937,7 +3079,7 @@ export class ClaudeAcpAgent {
2937
3079
  return;
2938
3080
  }
2939
3081
  if (!supportsAirSessionFailures(this.clientCapabilities)) {
2940
- failActive(error);
3082
+ failActive(error, title);
2941
3083
  return;
2942
3084
  }
2943
3085
  if (!session.activeTurn || session.activeTurn.settled) {
@@ -2947,7 +3089,7 @@ export class ClaudeAcpAgent {
2947
3089
  }
2948
3090
  const failure = await createSessionFailure(kind, { title });
2949
3091
  if (!failure) {
2950
- failActive(error);
3092
+ failActive(error, title);
2951
3093
  return;
2952
3094
  }
2953
3095
  sessionFailures.recordActive(failure);
@@ -3051,7 +3193,7 @@ export class ClaudeAcpAgent {
3051
3193
  }
3052
3194
  }
3053
3195
  await compaction.interrupt();
3054
- await settleActive(turnOutcome(session, "cancelled"));
3196
+ await settleActive(cancelledOutcome(session, session.activeTurn));
3055
3197
  // The cancelled turn's result may never come (that's why the
3056
3198
  // backstop fired) — close its delivery stretch here so partial
3057
3199
  // streamed text can't suppress the next turn's issue-#453 fallback.
@@ -3107,7 +3249,7 @@ export class ClaudeAcpAgent {
3107
3249
  const inFlight = session.activeTurn;
3108
3250
  await finishLifecycle(session.cancelled ? "cancelled" : "failed", session.cancelled ? "stopped" : "failed", "at end of stream");
3109
3251
  await settleActive(session.cancelled
3110
- ? turnOutcome(session, "cancelled")
3252
+ ? cancelledOutcome(session, inFlight)
3111
3253
  : (inFlight?.deferredSettle ?? turnOutcome(session, stopReason)));
3112
3254
  // Queued turns the SDK never started never ran, so reject them rather
3113
3255
  // than reporting a success (end_turn) — or a misleading "cancelled" —
@@ -3142,10 +3284,10 @@ export class ClaudeAcpAgent {
3142
3284
  // prompt() stamps a uuid on every message. The frame is @internal and
3143
3285
  // absent from the SDKMessage union, so handle it BEFORE the exhaustive
3144
3286
  // switch: it must not reach `unreachable`'s error log, and a `case`
3145
- // for it wouldn't typecheck. It feeds only the orphan accounting (see
3146
- // Session.orphanCommands); turn settlement stays driven by
3147
- // echoes/results/idle. (Raw-mode emission above still forwards these
3148
- // frames.)
3287
+ // for it wouldn't typecheck. It feeds the orphan accounting (see
3288
+ // Session.orphanCommands) and reports a turn inserted at "started";
3289
+ // turn activation and settlement stay driven by echoes/results/idle.
3290
+ // (Raw-mode emission above still forwards these frames.)
3149
3291
  if (message.type === "command_lifecycle") {
3150
3292
  const frame = message;
3151
3293
  switch (frame.state) {
@@ -3156,6 +3298,17 @@ export class ClaudeAcpAgent {
3156
3298
  if (queued) {
3157
3299
  queued.commandStarted = true;
3158
3300
  compaction.resume();
3301
+ // Claude Code took the prompt in: it drained into a turn. A
3302
+ // fresh turn reports that before anything it produces, its echo
3303
+ // and first stream events included, so this is the earliest
3304
+ // insertion point. (A prompt folded into a running turn reports
3305
+ // it after its echo, which already activated it.) While another
3306
+ // turn is still active (held for background work, steered, or
3307
+ // cancelled and awaiting its trailing idle), insertion waits
3308
+ // for this turn's echo, whose hand-off ends that turn first.
3309
+ const active = session.activeTurn;
3310
+ if (!active || active.settled || active === queued)
3311
+ reportInserted(queued);
3159
3312
  }
3160
3313
  // ...and promote an already-orphaned command: once dispatched,
3161
3314
  // a bare `cancelled` no longer means "dropped without running".
@@ -3332,17 +3485,18 @@ export class ClaudeAcpAgent {
3332
3485
  if (compaction.consumeDuplicateErrorOutput(message.content)) {
3333
3486
  break;
3334
3487
  }
3335
- const usageTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
3336
- const usageMarkdown = await takeUsageMarkdown(message.content);
3337
- if (usageTurn?.isUsageCommand && session.cancelled)
3488
+ const commandTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
3489
+ const commandMarkdown = await takeLocalCommandMarkdown(message.content);
3490
+ if (commandTurn?.localCommand && session.cancelled)
3338
3491
  break;
3339
- if (usageMarkdown === null)
3492
+ if (commandMarkdown === null)
3340
3493
  break;
3341
3494
  await sendUpdate({
3342
3495
  sessionId: params.sessionId,
3343
3496
  update: {
3344
3497
  sessionUpdate: "agent_message_chunk",
3345
- content: { type: "text", text: usageMarkdown ?? message.content },
3498
+ content: { type: "text", text: commandMarkdown ?? message.content },
3499
+ messageId: messageIdForGrouping(message),
3346
3500
  },
3347
3501
  });
3348
3502
  break;
@@ -3399,7 +3553,13 @@ export class ClaudeAcpAgent {
3399
3553
  // the interrupted turn's tokens entirely (issue #844). Zero
3400
3554
  // when the cancel pre-empted the result (wedge/force-cancel).
3401
3555
  if (session.cancelled && session.activeTurn && !session.activeTurn.settled) {
3402
- await settleActive(turnOutcome(session, "cancelled"));
3556
+ // A held turn's result passed long ago; this idle is the
3557
+ // trailer of the followup cycle the cancel interrupted,
3558
+ // whose result (an autonomous one) recorded it as owed.
3559
+ if (isHeldOpen(session.activeTurn) && session.owedTrailingIdles > 0) {
3560
+ session.owedTrailingIdles--;
3561
+ }
3562
+ await settleActive(cancelledOutcome(session, session.activeTurn));
3403
3563
  // An interrupt can pre-empt the turn's result entirely
3404
3564
  // (nothing ran the result-case `finally`), so close the
3405
3565
  // delivery stretch here: idle is the SDK's authoritative
@@ -3501,11 +3661,12 @@ export class ClaudeAcpAgent {
3501
3661
  // list with this payload. Forward message.commands directly —
3502
3662
  // it's authoritative, and re-querying supportedCommands()
3503
3663
  // would just return the same list with an extra round-trip.
3664
+ session.skillPaths = new Map();
3504
3665
  await sendUpdate({
3505
3666
  sessionId: params.sessionId,
3506
3667
  update: {
3507
3668
  sessionUpdate: "available_commands_update",
3508
- availableCommands: getAvailableSlashCommands(message.commands, session.terminalSlashCommands),
3669
+ availableCommands: getAvailableSlashCommands(message.commands, session.terminalSlashCommands, this.toolCallCapabilities.air.client ? session.cwd : undefined, session.skillPaths),
3509
3670
  },
3510
3671
  });
3511
3672
  break;
@@ -4277,6 +4438,7 @@ export class ClaudeAcpAgent {
4277
4438
  update: {
4278
4439
  sessionUpdate: "agent_message_chunk",
4279
4440
  content: { type: "text", text: lastRefusalExplanation },
4441
+ messageId: messageIdForGrouping(message),
4280
4442
  },
4281
4443
  });
4282
4444
  }
@@ -4351,10 +4513,13 @@ export class ClaudeAcpAgent {
4351
4513
  (message.usage.output_tokens ?? 0) === 0 &&
4352
4514
  !session.activeTurn?.noticeTexts?.includes(normalizeNoticeText(message.result)));
4353
4515
  if (shouldForwardResult) {
4354
- const usageMarkdown = await takeUsageMarkdown(message.result);
4355
- if (usageMarkdown === null)
4516
+ const commandTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
4517
+ const commandMarkdown = await takeLocalCommandMarkdown(message.result);
4518
+ if (commandTurn?.localCommand && session.cancelled)
4519
+ break;
4520
+ if (commandMarkdown === null)
4356
4521
  break;
4357
- for (const notification of toAcpNotifications(usageMarkdown ?? message.result, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger)) {
4522
+ for (const notification of toAcpNotifications(commandMarkdown ?? message.result, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, { messageId: messageIdForGrouping(message) })) {
4358
4523
  await sendUpdate(notification);
4359
4524
  }
4360
4525
  }
@@ -4567,6 +4732,7 @@ export class ClaudeAcpAgent {
4567
4732
  }
4568
4733
  for (const notification of streamEventToAcpNotifications(message, params.sessionId, session.toolUseCache, this.client, this.logger, {
4569
4734
  clientCapabilities: this.clientCapabilities,
4735
+ toolCallCapabilities: this.toolCallCapabilities,
4570
4736
  cwd: session.cwd,
4571
4737
  taskState: session.taskState,
4572
4738
  emittedToolCalls: session.emittedToolCalls,
@@ -4636,7 +4802,7 @@ export class ClaudeAcpAgent {
4636
4802
  session.owedTrailingIdles++;
4637
4803
  // Before activateTurn resets the accumulator, so the
4638
4804
  // usage still belongs to the cancelled turn.
4639
- await settleActive(turnOutcome(session, "cancelled"));
4805
+ await settleActive(cancelledOutcome(session, session.activeTurn));
4640
4806
  }
4641
4807
  else if (isHeldOpen(session.activeTurn)) {
4642
4808
  // A turn held open for its background subagents (see
@@ -4747,16 +4913,16 @@ export class ClaudeAcpAgent {
4747
4913
  // Depending on the Claude Code build, a local command can arrive
4748
4914
  // as the dedicated system message above or as a synthetic
4749
4915
  // assistant message. Replace only the output owned by the exact
4750
- // /usage turn; no content signatures or text parsing are involved.
4916
+ // /usage or /mcp turn; no content signatures or text parsing are involved.
4751
4917
  if (message.type === "assistant" &&
4752
4918
  message.parent_tool_use_id === null &&
4753
4919
  message.message.model === "<synthetic>") {
4754
- const usageMarkdown = await takeUsageMarkdown(assistantMessageText(message.message) ?? "");
4920
+ const commandMarkdown = await takeLocalCommandMarkdown(assistantMessageText(message.message) ?? "");
4755
4921
  if (session.cancelled)
4756
4922
  break;
4757
- if (usageMarkdown !== undefined) {
4758
- if (usageMarkdown !== null) {
4759
- for (const notification of toAcpNotifications(usageMarkdown, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, { messageId: messageIdForGrouping(message) })) {
4923
+ if (commandMarkdown !== undefined) {
4924
+ if (commandMarkdown !== null) {
4925
+ for (const notification of toAcpNotifications(commandMarkdown, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, { messageId: messageIdForGrouping(message) })) {
4760
4926
  await sendUpdate(notification);
4761
4927
  }
4762
4928
  }
@@ -4770,6 +4936,7 @@ export class ClaudeAcpAgent {
4770
4936
  if (typeof stripped === "string") {
4771
4937
  for (const notification of toAcpNotifications(stripped, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
4772
4938
  clientCapabilities: this.clientCapabilities,
4939
+ toolCallCapabilities: this.toolCallCapabilities,
4773
4940
  parentToolUseId: message.parent_tool_use_id,
4774
4941
  cwd: session.cwd,
4775
4942
  taskState: session.taskState,
@@ -4851,6 +5018,7 @@ export class ClaudeAcpAgent {
4851
5018
  }
4852
5019
  for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
4853
5020
  clientCapabilities: this.clientCapabilities,
5021
+ toolCallCapabilities: this.toolCallCapabilities,
4854
5022
  parentToolUseId: message.parent_tool_use_id,
4855
5023
  cwd: session.cwd,
4856
5024
  taskState: session.taskState,
@@ -5040,6 +5208,36 @@ export class ClaudeAcpAgent {
5040
5208
  session.pendingOrphanResults = (session.pendingOrphanResults ?? 0) + 1;
5041
5209
  }
5042
5210
  }
5211
+ /** Ask Claude Code to drop user messages it still has queued, so that a
5212
+ * prompt a cancel settled is not run after all. Claude Code keeps queued
5213
+ * messages through an interrupt and runs them next (the receipt's
5214
+ * `still_queued`), unless they are cancelled first with
5215
+ * `cancel_async_message`: a no-op for a message it already dequeued, which
5216
+ * then still runs, as an orphan (see `orphanCommands`).
5217
+ *
5218
+ * Not awaited: the SDK writes control requests in call order, so the drops
5219
+ * reach Claude Code before a request sent after them, and a wedged CLI must
5220
+ * not hold up the cancel. The orphan accounting needs no update here: a
5221
+ * dropped message gets a `cancelled` lifecycle frame and is missing from the
5222
+ * interrupt receipt's `still_queued`, which both already account for. The
5223
+ * SDK implements the request as `Query.cancelAsyncMessage` without typing
5224
+ * it, so it is looked up, and a CLI that lacks it rejects the request. */
5225
+ dropQueuedMessages(sessionId, query, uuids) {
5226
+ const cancelAsyncMessage = query
5227
+ .cancelAsyncMessage;
5228
+ if (typeof cancelAsyncMessage !== "function")
5229
+ return;
5230
+ const logFailure = (uuid, error) => this.logger.error(`Session ${sessionId}: could not drop the queued message ${uuid} from Claude Code:`, error);
5231
+ for (const uuid of uuids) {
5232
+ // Called synchronously: deferring it would let the interrupt go first.
5233
+ try {
5234
+ Promise.resolve(cancelAsyncMessage.call(query, uuid)).catch((error) => logFailure(uuid, error));
5235
+ }
5236
+ catch (error) {
5237
+ logFailure(uuid, error);
5238
+ }
5239
+ }
5240
+ }
5043
5241
  async cancel(params) {
5044
5242
  await this.cancelTurns(params, { awaitInterrupt: true });
5045
5243
  }
@@ -5056,7 +5254,7 @@ export class ClaudeAcpAgent {
5056
5254
  }
5057
5255
  session.cancelled = true;
5058
5256
  for (const turn of session.turnQueue ?? [])
5059
- turn.usageMarkdownAbort?.abort();
5257
+ turn.localCommandAbort?.abort();
5060
5258
  session.pendingExitPlanModeInterruption = undefined;
5061
5259
  session.pendingExitPlanContextReset = undefined;
5062
5260
  // The stream already ended (see closeQueryStream): every in-flight turn was
@@ -5085,12 +5283,16 @@ export class ClaudeAcpAgent {
5085
5283
  session.toolCallFields?.clear();
5086
5284
  clearHookCallbacks(params.sessionId);
5087
5285
  }
5286
+ // The user messages this cancel abandons while Claude Code may still have
5287
+ // them queued: they are dropped from its queue before the interrupt.
5288
+ const abandoned = [];
5088
5289
  // A priority steer may still be queued in the SDK when cancellation
5089
5290
  // settles its owning turn. Its later echo matches no live turn, and its
5090
5291
  // result must be skipped rather than promoted onto the next prompt.
5091
5292
  if (isSteering(session.activeTurn)) {
5092
5293
  for (const uuid of session.activeTurn.steeredEchoes) {
5093
5294
  this.trackOrphanCommand(session, uuid, "pending");
5295
+ abandoned.push(uuid);
5094
5296
  }
5095
5297
  }
5096
5298
  // Capture the orphan-accounting lane before anything can await: the
@@ -5114,11 +5316,13 @@ export class ClaudeAcpAgent {
5114
5316
  // accumulator (the active turn's tally) is not its spend.
5115
5317
  turn.resolve({ stopReason: "cancelled" });
5116
5318
  orphanedTurns.push(turn);
5319
+ abandoned.push(turn.promptUuid);
5117
5320
  }
5118
5321
  }
5119
- // Each removed queued turn's user message was already pushed to the SDK,
5120
- // which processes input FIFO and will still emit a result for it with no
5121
- // user echo to match (0.3.246+ CLIs do stamp results with the
5322
+ // Each removed queued turn's user message was already pushed to the SDK.
5323
+ // It is dropped from Claude Code's queue below (see dropQueuedMessages),
5324
+ // but one Claude Code already dequeued still runs, FIFO, and emits a
5325
+ // result with no user echo to match (0.3.246+ CLIs do stamp results with the
5122
5326
  // triggering send's user_message_uuid, which ensureActiveTurn uses as
5123
5327
  // an exact join when present). Track those so the consumer skips them
5124
5328
  // (see ensureActiveTurn) rather than misattributing them to the head.
@@ -5181,15 +5385,23 @@ export class ClaudeAcpAgent {
5181
5385
  }
5182
5386
  // A deferred active turn (see Turn.deferredSettle) already has its result
5183
5387
  // and is only held open for subagents. A settling turn likewise has its
5184
- // result but is awaiting the bounded checkpoint preview. Settle either one
5185
- // "cancelled" NOW: the consumer cannot process the interrupt's trailing
5186
- // idle while blocked in that preview, and a held turn may already be idle.
5187
- // The captured outcome preserves usage and metadata (issue #844), while
5188
- // reporter state makes a late checkpoint response harmless.
5388
+ // result but is awaiting the bounded checkpoint preview. Settle it
5389
+ // "cancelled" NOW where the consumer cannot: it cannot process the
5390
+ // interrupt's trailing idle while blocked in that preview, and a held turn
5391
+ // whose session sits idle gets no trailing idle (the interrupt emits
5392
+ // nothing; with no state events consumed yet, the same is assumed).
5393
+ // A held turn whose followup cycle is live (running, or blocked on a
5394
+ // permission request) is left to that idle, like any live turn: the
5395
+ // interrupt still flushes the cycle's output, which must reach the client
5396
+ // before the turn ends cancelled. The captured outcome preserves usage and
5397
+ // metadata (issue #844), while reporter state makes a late checkpoint
5398
+ // response harmless.
5189
5399
  {
5190
5400
  const active = session.activeTurn;
5191
- const pendingOutcome = active?.deferredSettle ?? active?.settlingOutcome;
5192
- if (active && pendingOutcome && (isHeldOpen(active) || active.settling)) {
5401
+ const followupLive = session.lastSessionState === "running" || session.lastSessionState === "requires_action";
5402
+ if (active &&
5403
+ (active.settling || (isHeldOpen(active) && !followupLive)) &&
5404
+ (active.deferredSettle ?? active.settlingOutcome)) {
5193
5405
  session.fileChangeReporter?.finish(active.fileChangeReport, "cancelled");
5194
5406
  active.settled = true;
5195
5407
  // Mirror settleActive's invariants (it is consumer-scoped and
@@ -5203,17 +5415,16 @@ export class ClaudeAcpAgent {
5203
5415
  // text since the last boundary was its followups', and left latched
5204
5416
  // it would suppress a following replayed turn's issue-#453 fallback.
5205
5417
  session.emittedAssistantText = false;
5206
- // When the interrupt below pre-empts a live cycle — running, or
5207
- // blocked on a permission request (requires_action, the #866 shape
5208
- // users cancel out of) — it produces a trailer idle with no counted
5209
- // result; with the hold's own trailer typically already absorbed,
5210
- // that idle would be un-owed and could lag past the next prompt's
5211
- // echo — read as the fresh turn ending without a result (issue #825
5212
- // false-fail). Pre-count it unless the session sits idle: there the
5213
- // interrupt emits nothing, and a debt that never drains would mask
5214
- // one future #825 detection. (lastSessionState is last-CONSUMED, so
5215
- // both stale reads exist and both are accepted one-cycle windows: a
5216
- // running transition still in the backlog reads as stale idle and
5418
+ // When the interrupt below pre-empts a live cycle (a settling turn's,
5419
+ // or one no state event reported), it produces a trailer idle with no
5420
+ // counted result; that idle would be un-owed and could lag past the
5421
+ // next prompt's echo — read as the fresh turn ending without a result
5422
+ // (issue #825 false-fail).
5423
+ // Pre-count it unless the session sits idle: there the interrupt
5424
+ // emits nothing, and a debt that never drains would mask one future
5425
+ // #825 detection. (lastSessionState is last-CONSUMED, so both stale
5426
+ // reads exist and both are accepted one-cycle windows: a running
5427
+ // transition still in the backlog reads as stale idle and
5217
5428
  // under-counts — that false-fail additionally needs the trailer to
5218
5429
  // lag past the next echo — while a cycle already completed into the
5219
5430
  // backlog reads as stale non-idle and over-counts, masking one
@@ -5223,14 +5434,7 @@ export class ClaudeAcpAgent {
5223
5434
  if (session.lastSessionState !== "idle") {
5224
5435
  session.owedTrailingIdles++;
5225
5436
  }
5226
- // Carries the held outcome's `_meta` too: a deferred outcome's only
5227
- // metadata is its quota breakdown, the counterpart of the usage taken
5228
- // from it here.
5229
- active.resolve({
5230
- stopReason: "cancelled",
5231
- usage: pendingOutcome.usage,
5232
- ...(pendingOutcome._meta ? { _meta: pendingOutcome._meta } : {}),
5233
- });
5437
+ active.resolve(cancelledOutcome(session, active));
5234
5438
  }
5235
5439
  }
5236
5440
  // A steered active turn (see Turn.steeredEchoes) settles "cancelled" in the
@@ -5263,6 +5467,9 @@ export class ClaudeAcpAgent {
5263
5467
  cancelController.abort();
5264
5468
  }, this.forceCancelGraceMs);
5265
5469
  }
5470
+ // Before the interrupt: once it stops the running turn, Claude Code starts
5471
+ // the next queued message right away.
5472
+ this.dropQueuedMessages(params.sessionId, session.query, abandoned);
5266
5473
  const interrupt = session.query.interrupt();
5267
5474
  if (!options.awaitInterrupt) {
5268
5475
  // The caller closes the query next, which rejects the pending reply.
@@ -5498,13 +5705,15 @@ export class ClaudeAcpAgent {
5498
5705
  await this.applyConfigOptionValue(params.sessionId, session, params.configId, effectiveValue);
5499
5706
  return { configOptions: session.configOptions };
5500
5707
  }
5501
- async replaySessionHistory(sessionId, resumedMessages) {
5708
+ async replaySessionHistory(sessionId, resumedMessages, pending) {
5502
5709
  const replayStartedAt = performance.now();
5503
5710
  const toolUseCache = {};
5504
5711
  const messages = resumedMessages ?? (await getSessionMessages(sessionId));
5505
5712
  const historyLoadedAt = performance.now();
5506
5713
  this.logger.log(`[session/replay] sessionId=${sessionId} phase=read durationMs=${Math.round(historyLoadedAt - replayStartedAt)} messages=${messages.length}`);
5507
- const session = this.sessions[sessionId];
5714
+ // A pending replay has no session record yet. It uses the state that the record takes later.
5715
+ const replayState = () => pending?.state ?? this.sessions[sessionId];
5716
+ const session = replayState();
5508
5717
  // A replay rebuilds the client view, so its plan goes out again.
5509
5718
  if (session?.taskState)
5510
5719
  forgetPublishedTaskPlan(session.taskState);
@@ -5516,7 +5725,12 @@ export class ClaudeAcpAgent {
5516
5725
  sessionId,
5517
5726
  state: session.sessionFailureState,
5518
5727
  capabilities: this.clientCapabilities,
5519
- isCurrent: () => this.sessions[sessionId] === session,
5728
+ isCurrent: () => {
5729
+ const live = this.sessions[sessionId];
5730
+ if (!live)
5731
+ return pending !== undefined && !pending.stopped;
5732
+ return live.sessionFailureState === session.sessionFailureState;
5733
+ },
5520
5734
  sendUpdate: (notification) => this.client.sessionUpdate(notification),
5521
5735
  logger: this.logger,
5522
5736
  })
@@ -5671,7 +5885,7 @@ export class ClaudeAcpAgent {
5671
5885
  await replayAsyncTasks.taskNotification(notification);
5672
5886
  };
5673
5887
  const replayMessage = async (message) => {
5674
- if (isReplayHiddenMetaMessage(message)) {
5888
+ if (pending?.stopped || isReplayHiddenMetaMessage(message)) {
5675
5889
  return;
5676
5890
  }
5677
5891
  if (message.type === "user" &&
@@ -5685,7 +5899,7 @@ export class ClaudeAcpAgent {
5685
5899
  // a client-supplied id without an extra getSessionMessages read. Not read
5686
5900
  // yet (see Session.messageIdToUuid).
5687
5901
  const replayMessageId = messageIdForGrouping(message);
5688
- const replaySession = this.sessions[sessionId];
5902
+ const replaySession = replayState();
5689
5903
  if (replaySession && replayMessageId && message.uuid) {
5690
5904
  replaySession.messageIdToUuid.set(replayMessageId, message.uuid);
5691
5905
  }
@@ -5779,8 +5993,9 @@ export class ClaudeAcpAgent {
5779
5993
  registerHooks: false,
5780
5994
  replay: true,
5781
5995
  clientCapabilities: this.clientCapabilities,
5782
- cwd: this.sessions[sessionId]?.cwd,
5783
- taskState: this.sessions[sessionId]?.taskState,
5996
+ toolCallCapabilities: this.toolCallCapabilities,
5997
+ cwd: replayState()?.cwd,
5998
+ taskState: replayState()?.taskState,
5784
5999
  messageId: replayMessageId,
5785
6000
  parentToolUseId,
5786
6001
  })) {
@@ -5825,7 +6040,7 @@ export class ClaudeAcpAgent {
5825
6040
  };
5826
6041
  for (const message of messages)
5827
6042
  await replayMessage(message);
5828
- if (nativeReplayEnabled) {
6043
+ if (nativeReplayEnabled && !pending?.stopped) {
5829
6044
  // Claude history persists sidechain messages and Agent/Task tool uses,
5830
6045
  // but not task_started/task_updated lifecycle frames. Recover terminal
5831
6046
  // state from the launch tool_result. Missing results and malformed or
@@ -5862,18 +6077,40 @@ export class ClaudeAcpAgent {
5862
6077
  /** Mark a client request as blocking on user input for exactly the lifetime
5863
6078
  * of its promise. Steering consults this session-local count synchronously,
5864
6079
  * so a message arriving while any permission/elicitation card is open uses
5865
- * non-interrupting SDK delivery. */
6080
+ * non-interrupting SDK delivery. The active turn reports that it awaits the
6081
+ * user while any such request is open (see `syncAwaitingUser`). */
5866
6082
  async withPendingUserInput(sessionId, request) {
5867
6083
  const session = this.sessions[sessionId];
5868
6084
  if (!session)
5869
6085
  return request();
5870
6086
  session.pendingUserInputCount = (session.pendingUserInputCount ?? 0) + 1;
6087
+ this.syncAwaitingUser(session);
5871
6088
  try {
5872
6089
  return await request();
5873
6090
  }
5874
6091
  finally {
5875
6092
  session.pendingUserInputCount = Math.max(0, (session.pendingUserInputCount ?? 1) - 1);
5876
- }
6093
+ this.syncAwaitingUser(session);
6094
+ }
6095
+ }
6096
+ /** Report whether the session's active turn awaits the user: exactly while
6097
+ * a permission request or question is open in the session. A request does
6098
+ * not say which prompt it is for, and Claude Code can ask for a queued
6099
+ * prompt before the consumer activates it, so a request is not tied to the
6100
+ * turn that was active when it opened. Called when the count changes and
6101
+ * when a turn activates. */
6102
+ syncAwaitingUser(session) {
6103
+ const turn = session.activeTurn;
6104
+ if (!turn || turn.settled || !turn.insertedReported)
6105
+ return;
6106
+ const waiting = (session.pendingUserInputCount ?? 0) > 0;
6107
+ if (waiting === (turn.awaitingUser ?? false))
6108
+ return;
6109
+ turn.awaitingUser = waiting;
6110
+ if (waiting)
6111
+ turn.events.awaitingUser();
6112
+ else
6113
+ turn.events.resumed();
5877
6114
  }
5878
6115
  /** Forward a permission request to the client, wiring the tool call's
5879
6116
  * `signal` through as a `cancellationSignal`. When the turn is cancelled
@@ -6237,7 +6474,7 @@ export class ClaudeAcpAgent {
6237
6474
  sessionId,
6238
6475
  update: {
6239
6476
  sessionUpdate: "available_commands_update",
6240
- availableCommands: getAvailableSlashCommands(commands, session.terminalSlashCommands),
6477
+ availableCommands: getAvailableSlashCommands(commands, session.terminalSlashCommands, this.toolCallCapabilities.air.client ? session.cwd : undefined, (session.skillPaths ??= new Map())),
6241
6478
  },
6242
6479
  });
6243
6480
  }
@@ -6558,6 +6795,8 @@ export class ClaudeAcpAgent {
6558
6795
  }
6559
6796
  async getOrCreateSession(params, resumedSession) {
6560
6797
  const existingSession = this.sessions[params.sessionId];
6798
+ // A recreated live session keeps its mode, not the mode of the transcript.
6799
+ const livePermissionMode = existingSession?.modes.currentModeId;
6561
6800
  if (existingSession) {
6562
6801
  const fingerprint = computeSessionFingerprint(params);
6563
6802
  if (fingerprint === existingSession.sessionFingerprint) {
@@ -6573,9 +6812,6 @@ export class ClaudeAcpAgent {
6573
6812
  // up the new values.
6574
6813
  await this.teardownSession(params.sessionId);
6575
6814
  }
6576
- const resumedModelHint = resumedSession
6577
- ? resumedSession.model
6578
- : await readResumedModel(params.sessionId, this.logger);
6579
6815
  const response = await this.createSession({
6580
6816
  cwd: params.cwd,
6581
6817
  mcpServers: params.mcpServers ?? [],
@@ -6583,7 +6819,8 @@ export class ClaudeAcpAgent {
6583
6819
  _meta: params._meta,
6584
6820
  }, {
6585
6821
  resume: params.sessionId,
6586
- resumedModelHint,
6822
+ permissionMode: livePermissionMode,
6823
+ ...(resumedSession ? { resumedModelHint: resumedSession.model } : {}),
6587
6824
  });
6588
6825
  return {
6589
6826
  sessionId: response.sessionId,
@@ -6612,6 +6849,12 @@ export class ClaudeAcpAgent {
6612
6849
  throw RequestError.invalidParams({ cwd }, `\`cwd\` is not a directory: ${cwd}`);
6613
6850
  }
6614
6851
  }
6852
+ /** Whether the session forwards the text and the thinking of a subagent. */
6853
+ forwardsSubagentText(meta) {
6854
+ return (clientSupportsSubagents(this.clientCapabilities) ||
6855
+ supportsSubagentTranscript(this.clientCapabilities) ||
6856
+ meta?.claudeCode?.options?.forwardSubagentText === true);
6857
+ }
6615
6858
  async createSession(params, creationOpts = {}) {
6616
6859
  const createStartedAt = performance.now();
6617
6860
  // Validate `cwd` up front. The ACP spec requires an absolute path, and the
@@ -6649,10 +6892,15 @@ export class ClaudeAcpAgent {
6649
6892
  // sign-out respawn, provider rerouting), so fill the same fast local hint
6650
6893
  // here when the caller did not provide it. Never fall back to the slow
6651
6894
  // getContextUsage control request.
6895
+ // The same tail read gives the permission mode of the resumed session.
6652
6896
  let resumedModelHint = creationOpts.resumedModelHint;
6653
- if (creationOpts.resume !== undefined &&
6654
- !Object.prototype.hasOwnProperty.call(creationOpts, "resumedModelHint")) {
6655
- resumedModelHint = await readResumedModel(creationOpts.resume, this.logger);
6897
+ let resumedPermissionMode;
6898
+ if (creationOpts.resume !== undefined) {
6899
+ const withModel = !Object.prototype.hasOwnProperty.call(creationOpts, "resumedModelHint");
6900
+ const tail = await readResumedTail(creationOpts.resume, this.logger, withModel);
6901
+ if (withModel)
6902
+ resumedModelHint = tail.model;
6903
+ resumedPermissionMode = tail.permissionMode;
6656
6904
  timing.phase("resume-transcript");
6657
6905
  }
6658
6906
  const input = new Pushable();
@@ -6714,7 +6962,11 @@ export class ClaudeAcpAgent {
6714
6962
  const allowBypass = ALLOW_BYPASS &&
6715
6963
  settingsManager.getSettings().permissions?.disableBypassPermissionsMode !== "disable" &&
6716
6964
  sessionMeta?.claudeCode?.options?.allowDangerouslySkipPermissions !== false;
6717
- const initialPermissionMode = resolvePermissionMode(creationOpts.permissionMode ?? settingsManager.getSettings().permissions?.defaultMode, this.logger, allowBypass);
6965
+ const initialPermissionMode = resolveInitialPermissionMode({
6966
+ explicit: creationOpts.permissionMode,
6967
+ resumed: resumedPermissionMode,
6968
+ defaultMode: settingsManager.getSettings().permissions?.defaultMode,
6969
+ }, this.logger, allowBypass);
6718
6970
  const userProvidedOptions = sessionMeta?.claudeCode?.options
6719
6971
  ? { ...sessionMeta.claudeCode.options }
6720
6972
  : undefined;
@@ -6723,9 +6975,7 @@ export class ClaudeAcpAgent {
6723
6975
  // hidden state that the Client cannot inspect or change.
6724
6976
  if (userProvidedOptions)
6725
6977
  delete userProvidedOptions.agent;
6726
- const forwardSubagentText = clientSupportsSubagents(this.clientCapabilities) ||
6727
- supportsSubagentTranscript(this.clientCapabilities) ||
6728
- userProvidedOptions?.forwardSubagentText === true;
6978
+ const forwardSubagentText = this.forwardsSubagentText(params._meta);
6729
6979
  // Configure thinking behavior from environment variable
6730
6980
  const thinking = resolveThinkingConfig(process.env.MAX_THINKING_TOKENS, this.logger);
6731
6981
  // Parse model configuration from environment (e.g. Bedrock model overrides)
@@ -6751,7 +7001,7 @@ export class ClaudeAcpAgent {
6751
7001
  // Per-session task state. Created here (rather than in the session record
6752
7002
  // below) so the TaskCreated/TaskCompleted hook callbacks can close over
6753
7003
  // the same Map that the streaming message handler will read from.
6754
- const taskState = new Map();
7004
+ const taskState = creationOpts.replayState?.taskState ?? new Map();
6755
7005
  // Resolve every workspace root once. The native checkpoint report uses
6756
7006
  // this same set for lexical path validation, and the SDK receives it below.
6757
7007
  const acpAdditionalDirectories = params.additionalDirectories ?? sessionMeta?.additionalRoots ?? [];
@@ -7044,7 +7294,7 @@ export class ClaudeAcpAgent {
7044
7294
  const allowedModels = Array.isArray(settingsAvailableModels)
7045
7295
  ? applyAvailableModelsAllowlist(initializationResult.models, settingsAvailableModels, settingsModelOverrides, settingsManager.getManagedDeniedModels())
7046
7296
  : initializationResult.models;
7047
- const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, resumedModelHint);
7297
+ const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, await resumedModelHint);
7048
7298
  timing.phase("models");
7049
7299
  // Resolve the current model's capabilities separately from the stable
7050
7300
  // permission-mode catalog advertised to ACP clients.
@@ -7193,8 +7443,8 @@ export class ClaudeAcpAgent {
7193
7443
  nativeSubagentParentByToolUseId: new Map(),
7194
7444
  emittedAssistantText: false,
7195
7445
  owedTrailingIdles: 0,
7196
- messageIdToUuid: new Map(),
7197
- sessionFailureState: createSessionFailureState(),
7446
+ messageIdToUuid: creationOpts.replayState?.messageIdToUuid ?? new Map(),
7447
+ sessionFailureState: creationOpts.replayState?.sessionFailureState ?? createSessionFailureState(),
7198
7448
  claudeSubscriptionGuard,
7199
7449
  accountKind: fromAccountInfo(initializationResult.account)?.kind,
7200
7450
  fileChangeReporter,
@@ -7235,7 +7485,10 @@ export class ClaudeAcpAgent {
7235
7485
  this.closeQueryStream(session);
7236
7486
  delete this.sessions[sessionId];
7237
7487
  try {
7238
- await this.createSession(session.creationParams, { resume: sessionId });
7488
+ await this.createSession(session.creationParams, {
7489
+ resume: sessionId,
7490
+ permissionMode: session.modes.currentModeId,
7491
+ });
7239
7492
  }
7240
7493
  catch (error) {
7241
7494
  // One session that cannot come back must not abort the switch. The
@@ -7329,6 +7582,20 @@ function turnOutcome(session, stopReason, extraMeta) {
7329
7582
  _meta: { ...turnQuotaMeta(session), ...extraMeta },
7330
7583
  };
7331
7584
  }
7585
+ /** The outcome of `turn` when a cancel ends it. A turn that already has its
7586
+ * result, because it is held open for its background subagents (see
7587
+ * Turn.deferredSettle) or is settling (see Turn.settlingOutcome), keeps the
7588
+ * usage and metadata that result recorded (issue #844). */
7589
+ function cancelledOutcome(session, turn) {
7590
+ const recorded = turn?.deferredSettle ?? turn?.settlingOutcome;
7591
+ if (!recorded)
7592
+ return turnOutcome(session, "cancelled");
7593
+ return {
7594
+ stopReason: "cancelled",
7595
+ usage: recorded.usage,
7596
+ ...(recorded._meta ? { _meta: recorded._meta } : {}),
7597
+ };
7598
+ }
7332
7599
  /** `_meta.quota` for a prompt response: what the turn spent, shaped like
7333
7600
  * codex-acp's (snake_case container keys, camelCase counters) so a client
7334
7601
  * reads one shape from either agent.
@@ -7716,7 +7983,20 @@ function getAvailableSlashCommands(commands,
7716
7983
  // Names the CLI tagged terminal-bound on `system`/init (their UX lives in
7717
7984
  // the CLI's own terminal, which ACP clients aren't) — filtered alongside
7718
7985
  // the static list. Raw CLI names, matched before the MCP rename.
7719
- terminalCommands) {
7986
+ terminalCommands,
7987
+ // The session cwd of an AIR client, or undefined for every other client.
7988
+ // AIR gets the kind and the SKILL.md path of each skill command.
7989
+ airSkillCwd,
7990
+ // The resolved SKILL.md paths of the session, keyed by the cwd and the name.
7991
+ skillPaths) {
7992
+ const skillPath = (name, cwd) => {
7993
+ const key = `${cwd}\0${name}`;
7994
+ if (skillPaths?.has(key))
7995
+ return skillPaths.get(key);
7996
+ const resolved = resolveSkillPath(name, cwd);
7997
+ skillPaths?.set(key, resolved);
7998
+ return resolved;
7999
+ };
7720
8000
  const UNSUPPORTED_COMMANDS = [
7721
8001
  "clear",
7722
8002
  "cost",
@@ -7727,8 +8007,10 @@ terminalCommands) {
7727
8007
  "release-notes",
7728
8008
  "todos",
7729
8009
  ];
7730
- return commands
7731
- .filter((command) => !terminalCommands?.includes(command.name))
8010
+ // The adapter replaces the terminal text of `/mcp` and runs its actions,
8011
+ // even when Claude Code tags the command terminal-bound.
8012
+ const advertised = commands
8013
+ .filter((command) => command.name !== "mcp" && !terminalCommands?.includes(command.name))
7732
8014
  .map((command) => {
7733
8015
  const input = command.argumentHint
7734
8016
  ? {
@@ -7738,16 +8020,26 @@ terminalCommands) {
7738
8020
  }
7739
8021
  : null;
7740
8022
  let name = command.name;
7741
- if (command.name.endsWith(" (MCP)")) {
8023
+ const mcpPrompt = command.name.endsWith(" (MCP)");
8024
+ if (mcpPrompt) {
7742
8025
  name = `mcp:${name.replace(" (MCP)", "")}`;
7743
8026
  }
8027
+ const skillMdPath = airSkillCwd && !command.builtin && !mcpPrompt
8028
+ ? skillPath(command.name, airSkillCwd)
8029
+ : undefined;
7744
8030
  return {
7745
8031
  name,
7746
8032
  description: command.description || "",
7747
8033
  input,
8034
+ ...(skillMdPath
8035
+ ? {
8036
+ _meta: withAirMeta(withAirMeta(undefined, AIR_KIND_KEY, "skill"), AIR_SKILL_PATH_KEY, skillMdPath),
8037
+ }
8038
+ : {}),
7748
8039
  };
7749
8040
  })
7750
8041
  .filter((command) => !UNSUPPORTED_COMMANDS.includes(command.name));
8042
+ return [...advertised, MCP_AVAILABLE_COMMAND];
7751
8043
  }
7752
8044
  function formatUriAsLink(uri) {
7753
8045
  try {
@@ -7922,10 +8214,19 @@ function toolCallFieldsOf(session) {
7922
8214
  * Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
7923
8215
  * Only handles text, image, and thinking chunks for now.
7924
8216
  */
8217
+ /**
8218
+ * The renderer of the notification functions: the agent's tool call
8219
+ * capabilities when they are passed, else those of the ACP capabilities.
8220
+ */
8221
+ function toolCallRenderer(options) {
8222
+ return options?.toolCallCapabilities
8223
+ ? new AcpToolCallRenderer(options.toolCallCapabilities, options.replay)
8224
+ : AcpToolCallRenderer.for(options?.clientCapabilities, options?.replay);
8225
+ }
7925
8226
  export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger, options) {
7926
8227
  const taskState = options?.taskState ?? new Map();
7927
8228
  const registerHooks = options?.registerHooks !== false;
7928
- const renderer = AcpToolCallRenderer.for(options?.clientCapabilities, options?.replay);
8229
+ const renderer = toolCallRenderer(options);
7929
8230
  if (typeof content === "string") {
7930
8231
  if (content.length === 0) {
7931
8232
  return [];
@@ -8263,6 +8564,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
8263
8564
  const streamedToolInputs = options?.streamedToolInputs;
8264
8565
  const forwardedOptions = {
8265
8566
  clientCapabilities: options?.clientCapabilities,
8567
+ toolCallCapabilities: options?.toolCallCapabilities,
8266
8568
  parentToolUseId: message.parent_tool_use_id,
8267
8569
  cwd: options?.cwd,
8268
8570
  taskState: options?.taskState,
@@ -8326,7 +8628,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
8326
8628
  // TodoWrite and the Task* tools never surfaced a tool_call to refine.
8327
8629
  if (!shouldEmitToolCall(streamedInput.name))
8328
8630
  return [];
8329
- const update = AcpToolCallRenderer.for(options?.clientCapabilities).partialRefinement(streamedInput, input, options?.cwd);
8631
+ const update = toolCallRenderer(options).partialRefinement(streamedInput, input, options?.cwd);
8330
8632
  if (message.parent_tool_use_id)
8331
8633
  stampParentToolUseId(update, message.parent_tool_use_id);
8332
8634
  applyMessageId(update, options?.messageId);