@agentclientprotocol/claude-agent-acp 0.85.2-preview.2 → 0.85.2-preview.21
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.
- package/README.md +1 -0
- package/dist/acp-agent.d.ts +100 -23
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +528 -183
- package/dist/air-extension.d.ts +2 -0
- package/dist/air-extension.d.ts.map +1 -1
- package/dist/air-extension.js +2 -0
- package/dist/clear-context-coordinator.d.ts +1 -0
- package/dist/clear-context-coordinator.d.ts.map +1 -1
- package/dist/clear-context-coordinator.js +2 -1
- package/dist/diff.d.ts +45 -8
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +111 -30
- package/dist/mcp-command.d.ts +51 -0
- package/dist/mcp-command.d.ts.map +1 -0
- package/dist/mcp-command.js +321 -0
- package/dist/permissions/presentation.d.ts +17 -2
- package/dist/permissions/presentation.d.ts.map +1 -1
- package/dist/permissions/presentation.js +4 -0
- package/dist/resumed-session.d.ts +16 -6
- package/dist/resumed-session.d.ts.map +1 -1
- package/dist/resumed-session.js +91 -25
- package/dist/session-config-ids.d.ts +1 -0
- package/dist/session-config-ids.d.ts.map +1 -1
- package/dist/session-config-ids.js +1 -0
- package/dist/session-mode.d.ts +14 -0
- package/dist/session-mode.d.ts.map +1 -1
- package/dist/session-mode.js +22 -0
- package/dist/tool-calls/client-capabilities.d.ts +20 -2
- package/dist/tool-calls/client-capabilities.d.ts.map +1 -1
- package/dist/tool-calls/client-capabilities.js +14 -4
- package/dist/tool-calls/facts.d.ts +8 -1
- package/dist/tool-calls/facts.d.ts.map +1 -1
- package/dist/tool-calls/renderer.d.ts +5 -1
- package/dist/tool-calls/renderer.d.ts.map +1 -1
- package/dist/tool-calls/renderer.js +14 -3
- package/dist/tool-calls/reporters/bash.d.ts.map +1 -1
- package/dist/tool-calls/reporters/bash.js +32 -5
- package/dist/tool-calls/reporters/file-edit.d.ts +1 -1
- package/dist/tool-calls/reporters/file-edit.d.ts.map +1 -1
- package/dist/tool-calls/reporters/file-edit.js +20 -14
- package/dist/turn-events.d.ts +11 -5
- package/dist/turn-events.d.ts.map +1 -1
- package/dist/usage-markdown.d.ts +2 -0
- package/dist/usage-markdown.d.ts.map +1 -1
- package/dist/usage-markdown.js +2 -1
- package/dist/utils.d.ts +12 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +28 -0
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +121 -17
- package/dist/v2/initialize.d.ts +2 -5
- package/dist/v2/initialize.d.ts.map +1 -1
- package/dist/v2/initialize.js +23 -6
- package/dist/v2/permission.d.ts +25 -0
- package/dist/v2/permission.d.ts.map +1 -0
- package/dist/v2/permission.js +38 -0
- package/dist/v2/prompt.d.ts +47 -0
- package/dist/v2/prompt.d.ts.map +1 -0
- package/dist/v2/prompt.js +153 -0
- package/dist/v2/session-update.d.ts +6 -0
- package/dist/v2/session-update.d.ts.map +1 -1
- package/dist/v2/session-update.js +27 -0
- package/dist/v2/session.d.ts +15 -4
- package/dist/v2/session.d.ts.map +1 -1
- package/dist/v2/session.js +20 -6
- package/dist/v2/terminal.d.ts +42 -0
- package/dist/v2/terminal.d.ts.map +1 -0
- package/dist/v2/terminal.js +95 -0
- package/dist/v2/tool-call.d.ts +38 -0
- package/dist/v2/tool-call.d.ts.map +1 -0
- package/dist/v2/tool-call.js +49 -0
- 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,12 +16,12 @@ 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 {
|
|
19
|
+
import { readResumedSession, readResumedTail, } from "./resumed-session.js";
|
|
20
20
|
import { SessionTiming } from "./session-timing.js";
|
|
21
|
-
import { ALLOW_BYPASS
|
|
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
|
-
import { buildClaudePermissionPresentation } from "./permissions/presentation.js";
|
|
24
|
+
import { buildClaudePermissionPresentation, } from "./permissions/presentation.js";
|
|
25
25
|
import { decodeClaudePermissionResponse } from "./permissions/response.js";
|
|
26
26
|
import { SettingsManager } from "./settings.js";
|
|
27
27
|
import { activeUsageLimitMessage, airSessionFailureCapabilityMeta, assistantMessageText, createSessionFailureState, isSyntheticUsageLimitMessage, providerFailureCategory, SessionFailureController, sessionFailureMeta, supportsAirSessionFailures, } from "./session-failure-extension.js";
|
|
@@ -36,11 +36,15 @@ 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 {
|
|
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";
|
|
42
|
+
import { DEFAULT_AGENT_ID } from "./session-config-ids.js";
|
|
41
43
|
import { parseToolResultMeta } from "./tool-result-meta.js";
|
|
42
44
|
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "./usage-markdown.js";
|
|
43
|
-
|
|
45
|
+
export { DEFAULT_AGENT_ID } from "./session-config-ids.js";
|
|
46
|
+
import { MCP_AVAILABLE_COMMAND, parseMcpCommand, runMcpCommand } from "./mcp-command.js";
|
|
47
|
+
import { MODE_CONFIG_ID, resolveInitialPermissionMode, SessionModeManager, } from "./session-mode.js";
|
|
44
48
|
import { applyAvailableModelsAllowlist, buildModelConfigOption, getAvailableModels, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
|
|
45
49
|
import { buildEffortConfigOption, EFFORT_CONFIG_ID, effortFlagSettings, mergeEffortSettings, settingsEffortForModel, } from "./session-effort.js";
|
|
46
50
|
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
|
|
@@ -77,6 +81,10 @@ const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
|
77
81
|
* pre-empt a slow-but-healthy interrupt. */
|
|
78
82
|
const DEFAULT_FORCE_CANCEL_GRACE_MS = 30_000;
|
|
79
83
|
const STRUCTURED_USAGE_TIMEOUT_MS = 5_000;
|
|
84
|
+
/** How long a session teardown waits for the turns that it cancels to end.
|
|
85
|
+
* The teardown wakes the consumer itself, so they end within a few ticks
|
|
86
|
+
* unless the consumer is wedged. */
|
|
87
|
+
const TEARDOWN_TURN_END_TIMEOUT_MS = 5_000;
|
|
80
88
|
/** The number of settled subagents whose parent tool call a session keeps for
|
|
81
89
|
* a later resume (see `resumableSubagents`). */
|
|
82
90
|
const MAX_RESUMABLE_SUBAGENTS = 256;
|
|
@@ -105,32 +113,18 @@ function settleLiveBackgroundTask(session, taskId) {
|
|
|
105
113
|
async function structuredUsageMarkdown(query, signal, logger) {
|
|
106
114
|
if (signal.aborted)
|
|
107
115
|
return null;
|
|
108
|
-
let timeout;
|
|
109
|
-
let onAbort;
|
|
110
116
|
try {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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) {
|
|
117
|
+
const outcome = await raceTimeoutAndAbort(
|
|
118
|
+
// Keeping the deliberately unstable method name visible makes an SDK
|
|
119
|
+
// upgrade fail at compile time if Anthropic removes or renames it.
|
|
120
|
+
query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(), STRUCTURED_USAGE_TIMEOUT_MS, signal);
|
|
121
|
+
if (outcome.type !== "done") {
|
|
122
|
+
if (outcome.type === "timeout") {
|
|
129
123
|
logger.error("Structured /usage timed out; preserving Claude Code output");
|
|
130
124
|
}
|
|
131
125
|
return null;
|
|
132
126
|
}
|
|
133
|
-
const usage = parseUsageResponse(
|
|
127
|
+
const usage = parseUsageResponse(outcome.value);
|
|
134
128
|
if (!usage) {
|
|
135
129
|
logger.error("Structured /usage returned an incompatible response; preserving Claude Code output");
|
|
136
130
|
return null;
|
|
@@ -141,12 +135,6 @@ async function structuredUsageMarkdown(query, signal, logger) {
|
|
|
141
135
|
logger.error(`Structured /usage failed; preserving Claude Code output: ${error}`);
|
|
142
136
|
return null;
|
|
143
137
|
}
|
|
144
|
-
finally {
|
|
145
|
-
if (timeout)
|
|
146
|
-
clearTimeout(timeout);
|
|
147
|
-
if (onAbort)
|
|
148
|
-
signal.removeEventListener("abort", onAbort);
|
|
149
|
-
}
|
|
150
138
|
}
|
|
151
139
|
/** Claude Code keeps the OAuth callback listener open in the background after
|
|
152
140
|
* `mcpAuthenticate` returns the authorization URL. The SDK does not expose
|
|
@@ -680,6 +668,7 @@ const REPLAY_HIDDEN_COMMANDS = new Set([
|
|
|
680
668
|
...LOCAL_ONLY_COMMANDS,
|
|
681
669
|
"/compact",
|
|
682
670
|
"/model",
|
|
671
|
+
"/mcp",
|
|
683
672
|
"/status",
|
|
684
673
|
"/usage",
|
|
685
674
|
]);
|
|
@@ -1017,12 +1006,19 @@ const mcpAuthentications = new WeakMap();
|
|
|
1017
1006
|
* authentication. This runs after session creation has returned so an
|
|
1018
1007
|
* interactive browser flow never delays `session/new`. */
|
|
1019
1008
|
function startMcpAuthentication(host, sessionId, mcpServers) {
|
|
1020
|
-
if (
|
|
1009
|
+
if (mcpServers.length === 0)
|
|
1010
|
+
return;
|
|
1011
|
+
startMcpServerAuthentication(host, sessionId, new Set(mcpServers.map((server) => server.name)));
|
|
1012
|
+
}
|
|
1013
|
+
/** Start OAuth in the background for the servers in `requestedServers` that
|
|
1014
|
+
* Claude reports as needing authentication. Startup and `/mcp` use it. It
|
|
1015
|
+
* does nothing while another OAuth run of the session is in progress. */
|
|
1016
|
+
function startMcpServerAuthentication(host, sessionId, requestedServers) {
|
|
1017
|
+
if (!host.clientCapabilities?.elicitation?.url)
|
|
1021
1018
|
return;
|
|
1022
1019
|
const session = host.sessions[sessionId];
|
|
1023
1020
|
if (!session || mcpAuthentications.has(session))
|
|
1024
1021
|
return;
|
|
1025
|
-
const requestedServers = new Set(mcpServers.map((server) => server.name));
|
|
1026
1022
|
const authentication = authenticateMcpServers(host, sessionId, session.query, requestedServers)
|
|
1027
1023
|
.catch((error) => {
|
|
1028
1024
|
if (!session.abortController.signal.aborted) {
|
|
@@ -1161,7 +1157,14 @@ export class ClaudeAcpAgent {
|
|
|
1161
1157
|
* return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
|
|
1162
1158
|
* tests can shrink it. */
|
|
1163
1159
|
forceCancelGraceMs = DEFAULT_FORCE_CANCEL_GRACE_MS;
|
|
1164
|
-
|
|
1160
|
+
/**
|
|
1161
|
+
* The connection serves ACP v2 (`src/v2/`). The agent still speaks v1
|
|
1162
|
+
* types, except for the tool call content that v2 needs and v1 cannot
|
|
1163
|
+
* express: see {@link ToolCallClientCapabilities.v2}.
|
|
1164
|
+
*/
|
|
1165
|
+
v2;
|
|
1166
|
+
constructor(client, logger, options = {}) {
|
|
1167
|
+
this.v2 = options.v2 ?? false;
|
|
1165
1168
|
this.sessions = {};
|
|
1166
1169
|
this.client = new ChangedMetaClient(client, () => this.toolCallCapabilities.air.client);
|
|
1167
1170
|
this.logger = logger ?? console;
|
|
@@ -1218,7 +1221,9 @@ export class ClaudeAcpAgent {
|
|
|
1218
1221
|
}
|
|
1219
1222
|
async initialize(request) {
|
|
1220
1223
|
this.clientCapabilities = request.clientCapabilities;
|
|
1221
|
-
this.toolCallCapabilities = ToolCallClientCapabilities.from(request.clientCapabilities
|
|
1224
|
+
this.toolCallCapabilities = ToolCallClientCapabilities.from(request.clientCapabilities, {
|
|
1225
|
+
v2: this.v2,
|
|
1226
|
+
});
|
|
1222
1227
|
// Learn the auth identity in the background: `initialize` never waits on
|
|
1223
1228
|
// the CLI probe, and no snapshot rides in its response. When the probe
|
|
1224
1229
|
// lands it calls `setAuthStatus`, which pushes `_auth/status_update` — the
|
|
@@ -1402,6 +1407,56 @@ export class ClaudeAcpAgent {
|
|
|
1402
1407
|
}, 0);
|
|
1403
1408
|
return response;
|
|
1404
1409
|
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Start the CLI of a loaded session, and replay the history while the CLI
|
|
1412
|
+
* starts. The CLI start takes most of the load time, and the replay needs
|
|
1413
|
+
* only the transcript and the {@link ReplayState}. An invalid `cwd` fails
|
|
1414
|
+
* before the first update. A failed CLI start stops the replay.
|
|
1415
|
+
*/
|
|
1416
|
+
async createSessionWhileReplaying(params, timing) {
|
|
1417
|
+
await this.validateCwd(params.cwd);
|
|
1418
|
+
const pending = {
|
|
1419
|
+
state: {
|
|
1420
|
+
cwd: params.cwd,
|
|
1421
|
+
taskState: new Map(),
|
|
1422
|
+
forwardSubagentText: this.forwardsSubagentText(params._meta),
|
|
1423
|
+
messageIdToUuid: new Map(),
|
|
1424
|
+
sessionFailureState: createSessionFailureState(),
|
|
1425
|
+
},
|
|
1426
|
+
stopped: false,
|
|
1427
|
+
};
|
|
1428
|
+
const transcript = readResumedSession(params.sessionId, this.logger);
|
|
1429
|
+
const replay = transcript
|
|
1430
|
+
.then(({ messages }) => this.replaySessionHistory(params.sessionId, messages, pending))
|
|
1431
|
+
.then(() => ({ ok: true }), (error) => ({ ok: false, error }));
|
|
1432
|
+
let response;
|
|
1433
|
+
try {
|
|
1434
|
+
response = await this.createSession({
|
|
1435
|
+
cwd: params.cwd,
|
|
1436
|
+
mcpServers: params.mcpServers ?? [],
|
|
1437
|
+
additionalDirectories: params.additionalDirectories,
|
|
1438
|
+
_meta: params._meta,
|
|
1439
|
+
}, {
|
|
1440
|
+
resume: params.sessionId,
|
|
1441
|
+
resumedModelHint: transcript.then(({ model }) => model),
|
|
1442
|
+
replayState: pending.state,
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
catch (error) {
|
|
1446
|
+
pending.stopped = true;
|
|
1447
|
+
await replay;
|
|
1448
|
+
throw error;
|
|
1449
|
+
}
|
|
1450
|
+
timing.phase("session-ready");
|
|
1451
|
+
const replayed = await replay;
|
|
1452
|
+
if (!replayed.ok)
|
|
1453
|
+
throw replayed.error;
|
|
1454
|
+
return {
|
|
1455
|
+
sessionId: response.sessionId,
|
|
1456
|
+
modes: response.modes,
|
|
1457
|
+
configOptions: response.configOptions,
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1405
1460
|
async unstable_forkSession(params) {
|
|
1406
1461
|
if (this.providerUpdate)
|
|
1407
1462
|
await this.providerUpdate;
|
|
@@ -1426,10 +1481,16 @@ export class ClaudeAcpAgent {
|
|
|
1426
1481
|
const timing = new SessionTiming(this.logger, "load", params.sessionId);
|
|
1427
1482
|
if (this.providerUpdate)
|
|
1428
1483
|
await this.providerUpdate;
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1484
|
+
let result;
|
|
1485
|
+
if (this.sessions[params.sessionId]) {
|
|
1486
|
+
const resumedSession = await readResumedSession(params.sessionId, this.logger);
|
|
1487
|
+
result = await this.getOrCreateSession(params, resumedSession);
|
|
1488
|
+
timing.phase("session-ready");
|
|
1489
|
+
await this.replaySessionHistory(params.sessionId, resumedSession.messages);
|
|
1490
|
+
}
|
|
1491
|
+
else {
|
|
1492
|
+
result = await this.createSessionWhileReplaying(params, timing);
|
|
1493
|
+
}
|
|
1433
1494
|
timing.phase("replay");
|
|
1434
1495
|
// Send available commands after replay so it doesn't interleave with history
|
|
1435
1496
|
setTimeout(() => {
|
|
@@ -1812,6 +1873,47 @@ export class ClaudeAcpAgent {
|
|
|
1812
1873
|
this.setAuthStatus(notLoggedInAuthStatus());
|
|
1813
1874
|
}
|
|
1814
1875
|
}
|
|
1876
|
+
/** The structured replacement for a prompt that is exactly `/usage` or a
|
|
1877
|
+
* `/mcp` command. Claude Code runs the command, and the replacement takes
|
|
1878
|
+
* the place of its text. Claude Code refuses a `/mcp` reconnect, enable,
|
|
1879
|
+
* or disable in SDK mode. The `/mcp` producer runs them through the SDK
|
|
1880
|
+
* control API when the text arrives, so the action stays in the turn. */
|
|
1881
|
+
localCommandMarkdown(params) {
|
|
1882
|
+
const text = params.prompt.length === 1 && params.prompt[0]?.type === "text"
|
|
1883
|
+
? params.prompt[0].text
|
|
1884
|
+
: undefined;
|
|
1885
|
+
if (text === undefined)
|
|
1886
|
+
return undefined;
|
|
1887
|
+
if (isUsageCommandText(text)) {
|
|
1888
|
+
return {
|
|
1889
|
+
startsAtActivation: true,
|
|
1890
|
+
produce: (query, signal) => structuredUsageMarkdown(query, signal, this.logger),
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
const command = parseMcpCommand(text);
|
|
1894
|
+
if (!command)
|
|
1895
|
+
return undefined;
|
|
1896
|
+
let reconnected = [];
|
|
1897
|
+
return {
|
|
1898
|
+
startsAtActivation: false,
|
|
1899
|
+
produce: async (query, signal, originalOutput) => {
|
|
1900
|
+
const outcome = await runMcpCommand(query, command, originalOutput, signal, (message) => this.logger.error(message));
|
|
1901
|
+
reconnected = outcome.reconnected;
|
|
1902
|
+
return outcome.markdown;
|
|
1903
|
+
},
|
|
1904
|
+
// A reconnect starts OAuth for the ACP servers that it tried, as the
|
|
1905
|
+
// session start does. A plain `/mcp` and a cancelled turn start none.
|
|
1906
|
+
afterDelivery: () => {
|
|
1907
|
+
const session = this.sessions[params.sessionId];
|
|
1908
|
+
if (!session || session.cancelled)
|
|
1909
|
+
return;
|
|
1910
|
+
const acpServers = new Set((session.creationParams?.mcpServers ?? []).map((server) => server.name));
|
|
1911
|
+
const servers = new Set(reconnected.filter((server) => acpServers.has(server)));
|
|
1912
|
+
if (servers.size > 0)
|
|
1913
|
+
startMcpServerAuthentication(this, params.sessionId, servers);
|
|
1914
|
+
},
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1815
1917
|
/**
|
|
1816
1918
|
* Serves an ACP v1 `session/prompt`, which answers when the turn ends. The
|
|
1817
1919
|
* outcome of a turn is field for field a v1 prompt response.
|
|
@@ -1878,10 +1980,8 @@ export class ClaudeAcpAgent {
|
|
|
1878
1980
|
const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
|
|
1879
1981
|
const isLocalOnlyCommand = firstText.startsWith("/") && LOCAL_ONLY_COMMANDS.has(firstText.split(" ", 1)[0]);
|
|
1880
1982
|
const fileChangeReport = session.fileChangeReporter?.request(params._meta);
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
isUsageCommandText(params.prompt[0].text);
|
|
1884
|
-
const usageMarkdownAbort = isUsageCommand ? new AbortController() : undefined;
|
|
1983
|
+
const localCommand = this.localCommandMarkdown(params);
|
|
1984
|
+
const localCommandAbort = localCommand ? new AbortController() : undefined;
|
|
1885
1985
|
session.titles.onPrompt(params.prompt);
|
|
1886
1986
|
// Each prompt is a Turn that the persistent consumer settles once the
|
|
1887
1987
|
// turn's outcome is known. `startTurn()` owns no loop: it enqueues the
|
|
@@ -1892,8 +1992,8 @@ export class ClaudeAcpAgent {
|
|
|
1892
1992
|
promptUuid,
|
|
1893
1993
|
events,
|
|
1894
1994
|
isLocalOnlyCommand,
|
|
1895
|
-
...(
|
|
1896
|
-
...(
|
|
1995
|
+
...(localCommand ? { localCommand } : {}),
|
|
1996
|
+
...(localCommandAbort ? { localCommandAbort } : {}),
|
|
1897
1997
|
...(fileChangeReport ? { fileChangeReport } : {}),
|
|
1898
1998
|
settled: false,
|
|
1899
1999
|
completion: new Promise((resolve) => {
|
|
@@ -1903,8 +2003,8 @@ export class ClaudeAcpAgent {
|
|
|
1903
2003
|
events.ended(outcome);
|
|
1904
2004
|
completeTurn();
|
|
1905
2005
|
},
|
|
1906
|
-
reject: (error) => {
|
|
1907
|
-
events.failed(error);
|
|
2006
|
+
reject: (error, title) => {
|
|
2007
|
+
events.failed(error, title);
|
|
1908
2008
|
completeTurn();
|
|
1909
2009
|
},
|
|
1910
2010
|
};
|
|
@@ -2015,7 +2115,10 @@ export class ClaudeAcpAgent {
|
|
|
2015
2115
|
try {
|
|
2016
2116
|
// `resume` names the Claude session, which shares the ACP session id, so
|
|
2017
2117
|
// the new query continues the same conversation under the same id.
|
|
2018
|
-
await this.createSession(creationParams, {
|
|
2118
|
+
await this.createSession(creationParams, {
|
|
2119
|
+
resume: sessionId,
|
|
2120
|
+
permissionMode: session.modes.currentModeId,
|
|
2121
|
+
});
|
|
2019
2122
|
}
|
|
2020
2123
|
catch (error) {
|
|
2021
2124
|
if (error instanceof RequestError && error.code === RequestError.resourceNotFound().code) {
|
|
@@ -2025,7 +2128,10 @@ export class ClaudeAcpAgent {
|
|
|
2025
2128
|
// better answer than a session the client can never use again.
|
|
2026
2129
|
this.logger.log(`session ${sessionId} was never persisted; starting a fresh query under the same id`);
|
|
2027
2130
|
try {
|
|
2028
|
-
await this.createSession(creationParams, {
|
|
2131
|
+
await this.createSession(creationParams, {
|
|
2132
|
+
reuseSessionId: sessionId,
|
|
2133
|
+
permissionMode: session.modes.currentModeId,
|
|
2134
|
+
});
|
|
2029
2135
|
return;
|
|
2030
2136
|
}
|
|
2031
2137
|
catch (fallbackError) {
|
|
@@ -2487,11 +2593,11 @@ export class ClaudeAcpAgent {
|
|
|
2487
2593
|
};
|
|
2488
2594
|
};
|
|
2489
2595
|
const internalErrorForClient = (data, rawDetail) => RequestError.internalError(data, supportsAirSessionFailures(this.clientCapabilities) ? undefined : rawDetail);
|
|
2490
|
-
const
|
|
2491
|
-
if (!turn.
|
|
2596
|
+
const ensureLocalCommandMarkdown = (turn, originalOutput = "") => {
|
|
2597
|
+
if (!turn.localCommand || !turn.localCommandAbort)
|
|
2492
2598
|
return undefined;
|
|
2493
|
-
turn.
|
|
2494
|
-
return turn.
|
|
2599
|
+
turn.localCommandMarkdown ??= turn.localCommand.produce(session.query, turn.localCommandAbort.signal, originalOutput);
|
|
2600
|
+
return turn.localCommandMarkdown;
|
|
2495
2601
|
};
|
|
2496
2602
|
const resetTurnScratch = () => {
|
|
2497
2603
|
lastAssistantTotalUsage = null;
|
|
@@ -2534,7 +2640,8 @@ export class ClaudeAcpAgent {
|
|
|
2534
2640
|
session.activeTurn = turn;
|
|
2535
2641
|
session.cancelled = false;
|
|
2536
2642
|
compaction.resume();
|
|
2537
|
-
|
|
2643
|
+
if (turn.localCommand?.startsAtActivation)
|
|
2644
|
+
ensureLocalCommandMarkdown(turn);
|
|
2538
2645
|
session.pendingOrphanResults = 0;
|
|
2539
2646
|
session.orphanCommands?.clear();
|
|
2540
2647
|
// Two-phase sweep of registry entries the level signal ended (see
|
|
@@ -2562,18 +2669,23 @@ export class ClaudeAcpAgent {
|
|
|
2562
2669
|
}
|
|
2563
2670
|
}
|
|
2564
2671
|
resetTurnScratch();
|
|
2565
|
-
//
|
|
2566
|
-
//
|
|
2567
|
-
//
|
|
2568
|
-
|
|
2569
|
-
if (!turn.insertedReported && !turn.settled) {
|
|
2570
|
-
turn.insertedReported = true;
|
|
2571
|
-
turn.events.inserted(turn.promptUuid);
|
|
2572
|
-
}
|
|
2672
|
+
// Without a command_lifecycle "started" frame before it (see that
|
|
2673
|
+
// handler), activation is the first sign that Claude Code took the
|
|
2674
|
+
// prompt in: its echo, or for an echo-less command, its result.
|
|
2675
|
+
reportInserted(turn);
|
|
2573
2676
|
// A request can already be open: Claude Code asks for a queued prompt
|
|
2574
2677
|
// before the consumer reaches its echo.
|
|
2575
2678
|
this.syncAwaitingUser(session);
|
|
2576
2679
|
};
|
|
2680
|
+
/** Report that Claude Code took the turn's prompt in, once. A turn can be
|
|
2681
|
+
* settled by now: an echo hand-off awaits settling the previous turn, and
|
|
2682
|
+
* a cancel in that window settles the queued one. */
|
|
2683
|
+
const reportInserted = (turn) => {
|
|
2684
|
+
if (turn.insertedReported || turn.settled)
|
|
2685
|
+
return;
|
|
2686
|
+
turn.insertedReported = true;
|
|
2687
|
+
turn.events.inserted(turn.promptUuid);
|
|
2688
|
+
};
|
|
2577
2689
|
/** Ensure there is an active turn before a user-turn result that carries no
|
|
2578
2690
|
* echo to activate it, by promoting the queue head. Most turns are
|
|
2579
2691
|
* activated by their replayed user message before their result, but some
|
|
@@ -2732,27 +2844,28 @@ export class ClaudeAcpAgent {
|
|
|
2732
2844
|
* the autonomous stretch-close guard. */
|
|
2733
2845
|
const firstUnsettledQueuedTurn = () => (session.turnQueue ?? []).find((t) => !t.settled);
|
|
2734
2846
|
/** Claim the structured replacement for the turn currently producing a
|
|
2735
|
-
* local-command output. Undefined means this is not a structured
|
|
2736
|
-
* turn (or its request failed), null means another SDK
|
|
2737
|
-
* already delivered it, and string is the one replacement
|
|
2738
|
-
|
|
2847
|
+
* local-command output. Undefined means this is not a structured
|
|
2848
|
+
* local-command turn (or its request failed), null means another SDK
|
|
2849
|
+
* message shape already delivered it, and string is the one replacement
|
|
2850
|
+
* to publish. */
|
|
2851
|
+
const takeLocalCommandMarkdown = async (originalOutput) => {
|
|
2739
2852
|
const turn = session.activeTurn ?? firstUnsettledQueuedTurn();
|
|
2740
2853
|
if (!turn)
|
|
2741
2854
|
return undefined;
|
|
2742
|
-
const
|
|
2743
|
-
if (!
|
|
2855
|
+
const pending = ensureLocalCommandMarkdown(turn, originalOutput);
|
|
2856
|
+
if (!pending)
|
|
2744
2857
|
return undefined;
|
|
2745
|
-
const markdown = await
|
|
2858
|
+
const markdown = await pending;
|
|
2746
2859
|
if (markdown === null)
|
|
2747
2860
|
return undefined;
|
|
2748
|
-
if (turn.
|
|
2861
|
+
if (turn.localCommandDelivered) {
|
|
2749
2862
|
// Different SDK message shapes can mirror the same local-command
|
|
2750
2863
|
// output. Suppress an exact mirror, but let a later, distinct frame
|
|
2751
2864
|
// (for example an interruption diagnostic) follow the normal path.
|
|
2752
|
-
return turn.
|
|
2865
|
+
return turn.localCommandOriginalOutput === originalOutput ? null : undefined;
|
|
2753
2866
|
}
|
|
2754
|
-
turn.
|
|
2755
|
-
turn.
|
|
2867
|
+
turn.localCommandDelivered = true;
|
|
2868
|
+
turn.localCommandOriginalOutput = originalOutput;
|
|
2756
2869
|
return markdown;
|
|
2757
2870
|
};
|
|
2758
2871
|
/** Registers a live background task (see `liveBackgroundTasks`). A
|
|
@@ -2905,7 +3018,9 @@ export class ClaudeAcpAgent {
|
|
|
2905
3018
|
// Captured before the settled flip below (isHeldOpen tests !settled).
|
|
2906
3019
|
const wasHeld = isHeldOpen(turn);
|
|
2907
3020
|
turn.settled = true;
|
|
2908
|
-
turn.
|
|
3021
|
+
turn.localCommandAbort?.abort();
|
|
3022
|
+
if (turn.localCommandDelivered)
|
|
3023
|
+
turn.localCommand?.afterDelivery?.();
|
|
2909
3024
|
disarmForceCancel(session);
|
|
2910
3025
|
session.turnQueue = (session.turnQueue ?? []).filter((t) => t !== turn);
|
|
2911
3026
|
session.activeTurn = null;
|
|
@@ -2929,7 +3044,7 @@ export class ClaudeAcpAgent {
|
|
|
2929
3044
|
};
|
|
2930
3045
|
/** Reject the active turn (auth required, error result, …) without tearing
|
|
2931
3046
|
* down the consumer: the stream continues to idle and later turns proceed. */
|
|
2932
|
-
const failActive = (error) => {
|
|
3047
|
+
const failActive = (error, title) => {
|
|
2933
3048
|
disarmForceCancel(session);
|
|
2934
3049
|
const turn = session.activeTurn;
|
|
2935
3050
|
if (!turn || turn.settled) {
|
|
@@ -2946,7 +3061,7 @@ export class ClaudeAcpAgent {
|
|
|
2946
3061
|
// start the next stretch clean, or its stale delivery record would
|
|
2947
3062
|
// suppress the next turn's issue-#453 result-text fallback.
|
|
2948
3063
|
session.emittedAssistantText = false;
|
|
2949
|
-
turn.reject(error);
|
|
3064
|
+
turn.reject(error, title);
|
|
2950
3065
|
};
|
|
2951
3066
|
/** Complete a negotiated terminal failure on the prompt response itself,
|
|
2952
3067
|
* which is the canonical AIR carrier. Legacy clients keep the historical
|
|
@@ -2970,7 +3085,7 @@ export class ClaudeAcpAgent {
|
|
|
2970
3085
|
return;
|
|
2971
3086
|
}
|
|
2972
3087
|
if (!supportsAirSessionFailures(this.clientCapabilities)) {
|
|
2973
|
-
failActive(error);
|
|
3088
|
+
failActive(error, title);
|
|
2974
3089
|
return;
|
|
2975
3090
|
}
|
|
2976
3091
|
if (!session.activeTurn || session.activeTurn.settled) {
|
|
@@ -2980,7 +3095,7 @@ export class ClaudeAcpAgent {
|
|
|
2980
3095
|
}
|
|
2981
3096
|
const failure = await createSessionFailure(kind, { title });
|
|
2982
3097
|
if (!failure) {
|
|
2983
|
-
failActive(error);
|
|
3098
|
+
failActive(error, title);
|
|
2984
3099
|
return;
|
|
2985
3100
|
}
|
|
2986
3101
|
sessionFailures.recordActive(failure);
|
|
@@ -3030,11 +3145,18 @@ export class ClaudeAcpAgent {
|
|
|
3030
3145
|
.then((result) => ({ kind: "message", result }));
|
|
3031
3146
|
const nextMessage = pendingNext;
|
|
3032
3147
|
// Fresh abort listener per iteration, removed when next() wins, so a
|
|
3033
|
-
// long-lived session doesn't accumulate listeners on one signal.
|
|
3148
|
+
// long-lived session doesn't accumulate listeners on one signal. An
|
|
3149
|
+
// abort that fired while the consumer was busy elsewhere, such as
|
|
3150
|
+
// sending an update, still wakes it: a listener added to an aborted
|
|
3151
|
+
// signal never fires. The consumer re-arms after each abort it
|
|
3152
|
+
// handles, so an aborted signal here is always an unhandled abort.
|
|
3034
3153
|
let onAbort;
|
|
3035
3154
|
const abortRace = new Promise((resolve) => {
|
|
3036
3155
|
onAbort = () => resolve("abort");
|
|
3037
|
-
cancelController.signal.
|
|
3156
|
+
if (cancelController.signal.aborted)
|
|
3157
|
+
onAbort();
|
|
3158
|
+
else
|
|
3159
|
+
cancelController.signal.addEventListener("abort", onAbort, { once: true });
|
|
3038
3160
|
});
|
|
3039
3161
|
const raced = await Promise.race([nextMessage, abortRace]);
|
|
3040
3162
|
cancelController.signal.removeEventListener("abort", onAbort);
|
|
@@ -3084,7 +3206,7 @@ export class ClaudeAcpAgent {
|
|
|
3084
3206
|
}
|
|
3085
3207
|
}
|
|
3086
3208
|
await compaction.interrupt();
|
|
3087
|
-
await settleActive(
|
|
3209
|
+
await settleActive(cancelledOutcome(session, session.activeTurn));
|
|
3088
3210
|
// The cancelled turn's result may never come (that's why the
|
|
3089
3211
|
// backstop fired) — close its delivery stretch here so partial
|
|
3090
3212
|
// streamed text can't suppress the next turn's issue-#453 fallback.
|
|
@@ -3140,7 +3262,7 @@ export class ClaudeAcpAgent {
|
|
|
3140
3262
|
const inFlight = session.activeTurn;
|
|
3141
3263
|
await finishLifecycle(session.cancelled ? "cancelled" : "failed", session.cancelled ? "stopped" : "failed", "at end of stream");
|
|
3142
3264
|
await settleActive(session.cancelled
|
|
3143
|
-
?
|
|
3265
|
+
? cancelledOutcome(session, inFlight)
|
|
3144
3266
|
: (inFlight?.deferredSettle ?? turnOutcome(session, stopReason)));
|
|
3145
3267
|
// Queued turns the SDK never started never ran, so reject them rather
|
|
3146
3268
|
// than reporting a success (end_turn) — or a misleading "cancelled" —
|
|
@@ -3175,10 +3297,10 @@ export class ClaudeAcpAgent {
|
|
|
3175
3297
|
// prompt() stamps a uuid on every message. The frame is @internal and
|
|
3176
3298
|
// absent from the SDKMessage union, so handle it BEFORE the exhaustive
|
|
3177
3299
|
// switch: it must not reach `unreachable`'s error log, and a `case`
|
|
3178
|
-
// for it wouldn't typecheck. It feeds
|
|
3179
|
-
// Session.orphanCommands)
|
|
3180
|
-
// echoes/results/idle.
|
|
3181
|
-
// frames.)
|
|
3300
|
+
// for it wouldn't typecheck. It feeds the orphan accounting (see
|
|
3301
|
+
// Session.orphanCommands) and reports a turn inserted at "started";
|
|
3302
|
+
// turn activation and settlement stay driven by echoes/results/idle.
|
|
3303
|
+
// (Raw-mode emission above still forwards these frames.)
|
|
3182
3304
|
if (message.type === "command_lifecycle") {
|
|
3183
3305
|
const frame = message;
|
|
3184
3306
|
switch (frame.state) {
|
|
@@ -3189,6 +3311,17 @@ export class ClaudeAcpAgent {
|
|
|
3189
3311
|
if (queued) {
|
|
3190
3312
|
queued.commandStarted = true;
|
|
3191
3313
|
compaction.resume();
|
|
3314
|
+
// Claude Code took the prompt in: it drained into a turn. A
|
|
3315
|
+
// fresh turn reports that before anything it produces, its echo
|
|
3316
|
+
// and first stream events included, so this is the earliest
|
|
3317
|
+
// insertion point. (A prompt folded into a running turn reports
|
|
3318
|
+
// it after its echo, which already activated it.) While another
|
|
3319
|
+
// turn is still active (held for background work, steered, or
|
|
3320
|
+
// cancelled and awaiting its trailing idle), insertion waits
|
|
3321
|
+
// for this turn's echo, whose hand-off ends that turn first.
|
|
3322
|
+
const active = session.activeTurn;
|
|
3323
|
+
if (!active || active.settled || active === queued)
|
|
3324
|
+
reportInserted(queued);
|
|
3192
3325
|
}
|
|
3193
3326
|
// ...and promote an already-orphaned command: once dispatched,
|
|
3194
3327
|
// a bare `cancelled` no longer means "dropped without running".
|
|
@@ -3365,17 +3498,18 @@ export class ClaudeAcpAgent {
|
|
|
3365
3498
|
if (compaction.consumeDuplicateErrorOutput(message.content)) {
|
|
3366
3499
|
break;
|
|
3367
3500
|
}
|
|
3368
|
-
const
|
|
3369
|
-
const
|
|
3370
|
-
if (
|
|
3501
|
+
const commandTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
|
|
3502
|
+
const commandMarkdown = await takeLocalCommandMarkdown(message.content);
|
|
3503
|
+
if (commandTurn?.localCommand && session.cancelled)
|
|
3371
3504
|
break;
|
|
3372
|
-
if (
|
|
3505
|
+
if (commandMarkdown === null)
|
|
3373
3506
|
break;
|
|
3374
3507
|
await sendUpdate({
|
|
3375
3508
|
sessionId: params.sessionId,
|
|
3376
3509
|
update: {
|
|
3377
3510
|
sessionUpdate: "agent_message_chunk",
|
|
3378
|
-
content: { type: "text", text:
|
|
3511
|
+
content: { type: "text", text: commandMarkdown ?? message.content },
|
|
3512
|
+
messageId: messageIdForGrouping(message),
|
|
3379
3513
|
},
|
|
3380
3514
|
});
|
|
3381
3515
|
break;
|
|
@@ -3432,7 +3566,13 @@ export class ClaudeAcpAgent {
|
|
|
3432
3566
|
// the interrupted turn's tokens entirely (issue #844). Zero
|
|
3433
3567
|
// when the cancel pre-empted the result (wedge/force-cancel).
|
|
3434
3568
|
if (session.cancelled && session.activeTurn && !session.activeTurn.settled) {
|
|
3435
|
-
|
|
3569
|
+
// A held turn's result passed long ago; this idle is the
|
|
3570
|
+
// trailer of the followup cycle the cancel interrupted,
|
|
3571
|
+
// whose result (an autonomous one) recorded it as owed.
|
|
3572
|
+
if (isHeldOpen(session.activeTurn) && session.owedTrailingIdles > 0) {
|
|
3573
|
+
session.owedTrailingIdles--;
|
|
3574
|
+
}
|
|
3575
|
+
await settleActive(cancelledOutcome(session, session.activeTurn));
|
|
3436
3576
|
// An interrupt can pre-empt the turn's result entirely
|
|
3437
3577
|
// (nothing ran the result-case `finally`), so close the
|
|
3438
3578
|
// delivery stretch here: idle is the SDK's authoritative
|
|
@@ -3534,11 +3674,12 @@ export class ClaudeAcpAgent {
|
|
|
3534
3674
|
// list with this payload. Forward message.commands directly —
|
|
3535
3675
|
// it's authoritative, and re-querying supportedCommands()
|
|
3536
3676
|
// would just return the same list with an extra round-trip.
|
|
3677
|
+
session.skillPaths = new Map();
|
|
3537
3678
|
await sendUpdate({
|
|
3538
3679
|
sessionId: params.sessionId,
|
|
3539
3680
|
update: {
|
|
3540
3681
|
sessionUpdate: "available_commands_update",
|
|
3541
|
-
availableCommands: getAvailableSlashCommands(message.commands, session.terminalSlashCommands),
|
|
3682
|
+
availableCommands: getAvailableSlashCommands(message.commands, session.terminalSlashCommands, this.toolCallCapabilities.air.client ? session.cwd : undefined, session.skillPaths),
|
|
3542
3683
|
},
|
|
3543
3684
|
});
|
|
3544
3685
|
break;
|
|
@@ -4310,6 +4451,7 @@ export class ClaudeAcpAgent {
|
|
|
4310
4451
|
update: {
|
|
4311
4452
|
sessionUpdate: "agent_message_chunk",
|
|
4312
4453
|
content: { type: "text", text: lastRefusalExplanation },
|
|
4454
|
+
messageId: messageIdForGrouping(message),
|
|
4313
4455
|
},
|
|
4314
4456
|
});
|
|
4315
4457
|
}
|
|
@@ -4384,10 +4526,13 @@ export class ClaudeAcpAgent {
|
|
|
4384
4526
|
(message.usage.output_tokens ?? 0) === 0 &&
|
|
4385
4527
|
!session.activeTurn?.noticeTexts?.includes(normalizeNoticeText(message.result)));
|
|
4386
4528
|
if (shouldForwardResult) {
|
|
4387
|
-
const
|
|
4388
|
-
|
|
4529
|
+
const commandTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
|
|
4530
|
+
const commandMarkdown = await takeLocalCommandMarkdown(message.result);
|
|
4531
|
+
if (commandTurn?.localCommand && session.cancelled)
|
|
4389
4532
|
break;
|
|
4390
|
-
|
|
4533
|
+
if (commandMarkdown === null)
|
|
4534
|
+
break;
|
|
4535
|
+
for (const notification of toAcpNotifications(commandMarkdown ?? message.result, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, { messageId: messageIdForGrouping(message) })) {
|
|
4391
4536
|
await sendUpdate(notification);
|
|
4392
4537
|
}
|
|
4393
4538
|
}
|
|
@@ -4600,6 +4745,7 @@ export class ClaudeAcpAgent {
|
|
|
4600
4745
|
}
|
|
4601
4746
|
for (const notification of streamEventToAcpNotifications(message, params.sessionId, session.toolUseCache, this.client, this.logger, {
|
|
4602
4747
|
clientCapabilities: this.clientCapabilities,
|
|
4748
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
4603
4749
|
cwd: session.cwd,
|
|
4604
4750
|
taskState: session.taskState,
|
|
4605
4751
|
emittedToolCalls: session.emittedToolCalls,
|
|
@@ -4669,7 +4815,7 @@ export class ClaudeAcpAgent {
|
|
|
4669
4815
|
session.owedTrailingIdles++;
|
|
4670
4816
|
// Before activateTurn resets the accumulator, so the
|
|
4671
4817
|
// usage still belongs to the cancelled turn.
|
|
4672
|
-
await settleActive(
|
|
4818
|
+
await settleActive(cancelledOutcome(session, session.activeTurn));
|
|
4673
4819
|
}
|
|
4674
4820
|
else if (isHeldOpen(session.activeTurn)) {
|
|
4675
4821
|
// A turn held open for its background subagents (see
|
|
@@ -4780,16 +4926,16 @@ export class ClaudeAcpAgent {
|
|
|
4780
4926
|
// Depending on the Claude Code build, a local command can arrive
|
|
4781
4927
|
// as the dedicated system message above or as a synthetic
|
|
4782
4928
|
// assistant message. Replace only the output owned by the exact
|
|
4783
|
-
// /usage turn; no content signatures or text parsing are involved.
|
|
4929
|
+
// /usage or /mcp turn; no content signatures or text parsing are involved.
|
|
4784
4930
|
if (message.type === "assistant" &&
|
|
4785
4931
|
message.parent_tool_use_id === null &&
|
|
4786
4932
|
message.message.model === "<synthetic>") {
|
|
4787
|
-
const
|
|
4933
|
+
const commandMarkdown = await takeLocalCommandMarkdown(assistantMessageText(message.message) ?? "");
|
|
4788
4934
|
if (session.cancelled)
|
|
4789
4935
|
break;
|
|
4790
|
-
if (
|
|
4791
|
-
if (
|
|
4792
|
-
for (const notification of toAcpNotifications(
|
|
4936
|
+
if (commandMarkdown !== undefined) {
|
|
4937
|
+
if (commandMarkdown !== null) {
|
|
4938
|
+
for (const notification of toAcpNotifications(commandMarkdown, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, { messageId: messageIdForGrouping(message) })) {
|
|
4793
4939
|
await sendUpdate(notification);
|
|
4794
4940
|
}
|
|
4795
4941
|
}
|
|
@@ -4803,6 +4949,7 @@ export class ClaudeAcpAgent {
|
|
|
4803
4949
|
if (typeof stripped === "string") {
|
|
4804
4950
|
for (const notification of toAcpNotifications(stripped, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
4805
4951
|
clientCapabilities: this.clientCapabilities,
|
|
4952
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
4806
4953
|
parentToolUseId: message.parent_tool_use_id,
|
|
4807
4954
|
cwd: session.cwd,
|
|
4808
4955
|
taskState: session.taskState,
|
|
@@ -4884,6 +5031,7 @@ export class ClaudeAcpAgent {
|
|
|
4884
5031
|
}
|
|
4885
5032
|
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
4886
5033
|
clientCapabilities: this.clientCapabilities,
|
|
5034
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
4887
5035
|
parentToolUseId: message.parent_tool_use_id,
|
|
4888
5036
|
cwd: session.cwd,
|
|
4889
5037
|
taskState: session.taskState,
|
|
@@ -5073,6 +5221,36 @@ export class ClaudeAcpAgent {
|
|
|
5073
5221
|
session.pendingOrphanResults = (session.pendingOrphanResults ?? 0) + 1;
|
|
5074
5222
|
}
|
|
5075
5223
|
}
|
|
5224
|
+
/** Ask Claude Code to drop user messages it still has queued, so that a
|
|
5225
|
+
* prompt a cancel settled is not run after all. Claude Code keeps queued
|
|
5226
|
+
* messages through an interrupt and runs them next (the receipt's
|
|
5227
|
+
* `still_queued`), unless they are cancelled first with
|
|
5228
|
+
* `cancel_async_message`: a no-op for a message it already dequeued, which
|
|
5229
|
+
* then still runs, as an orphan (see `orphanCommands`).
|
|
5230
|
+
*
|
|
5231
|
+
* Not awaited: the SDK writes control requests in call order, so the drops
|
|
5232
|
+
* reach Claude Code before a request sent after them, and a wedged CLI must
|
|
5233
|
+
* not hold up the cancel. The orphan accounting needs no update here: a
|
|
5234
|
+
* dropped message gets a `cancelled` lifecycle frame and is missing from the
|
|
5235
|
+
* interrupt receipt's `still_queued`, which both already account for. The
|
|
5236
|
+
* SDK implements the request as `Query.cancelAsyncMessage` without typing
|
|
5237
|
+
* it, so it is looked up, and a CLI that lacks it rejects the request. */
|
|
5238
|
+
dropQueuedMessages(sessionId, query, uuids) {
|
|
5239
|
+
const cancelAsyncMessage = query
|
|
5240
|
+
.cancelAsyncMessage;
|
|
5241
|
+
if (typeof cancelAsyncMessage !== "function")
|
|
5242
|
+
return;
|
|
5243
|
+
const logFailure = (uuid, error) => this.logger.error(`Session ${sessionId}: could not drop the queued message ${uuid} from Claude Code:`, error);
|
|
5244
|
+
for (const uuid of uuids) {
|
|
5245
|
+
// Called synchronously: deferring it would let the interrupt go first.
|
|
5246
|
+
try {
|
|
5247
|
+
Promise.resolve(cancelAsyncMessage.call(query, uuid)).catch((error) => logFailure(uuid, error));
|
|
5248
|
+
}
|
|
5249
|
+
catch (error) {
|
|
5250
|
+
logFailure(uuid, error);
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5076
5254
|
async cancel(params) {
|
|
5077
5255
|
await this.cancelTurns(params, { awaitInterrupt: true });
|
|
5078
5256
|
}
|
|
@@ -5089,7 +5267,7 @@ export class ClaudeAcpAgent {
|
|
|
5089
5267
|
}
|
|
5090
5268
|
session.cancelled = true;
|
|
5091
5269
|
for (const turn of session.turnQueue ?? [])
|
|
5092
|
-
turn.
|
|
5270
|
+
turn.localCommandAbort?.abort();
|
|
5093
5271
|
session.pendingExitPlanModeInterruption = undefined;
|
|
5094
5272
|
session.pendingExitPlanContextReset = undefined;
|
|
5095
5273
|
// The stream already ended (see closeQueryStream): every in-flight turn was
|
|
@@ -5118,12 +5296,16 @@ export class ClaudeAcpAgent {
|
|
|
5118
5296
|
session.toolCallFields?.clear();
|
|
5119
5297
|
clearHookCallbacks(params.sessionId);
|
|
5120
5298
|
}
|
|
5299
|
+
// The user messages this cancel abandons while Claude Code may still have
|
|
5300
|
+
// them queued: they are dropped from its queue before the interrupt.
|
|
5301
|
+
const abandoned = [];
|
|
5121
5302
|
// A priority steer may still be queued in the SDK when cancellation
|
|
5122
5303
|
// settles its owning turn. Its later echo matches no live turn, and its
|
|
5123
5304
|
// result must be skipped rather than promoted onto the next prompt.
|
|
5124
5305
|
if (isSteering(session.activeTurn)) {
|
|
5125
5306
|
for (const uuid of session.activeTurn.steeredEchoes) {
|
|
5126
5307
|
this.trackOrphanCommand(session, uuid, "pending");
|
|
5308
|
+
abandoned.push(uuid);
|
|
5127
5309
|
}
|
|
5128
5310
|
}
|
|
5129
5311
|
// Capture the orphan-accounting lane before anything can await: the
|
|
@@ -5147,11 +5329,13 @@ export class ClaudeAcpAgent {
|
|
|
5147
5329
|
// accumulator (the active turn's tally) is not its spend.
|
|
5148
5330
|
turn.resolve({ stopReason: "cancelled" });
|
|
5149
5331
|
orphanedTurns.push(turn);
|
|
5332
|
+
abandoned.push(turn.promptUuid);
|
|
5150
5333
|
}
|
|
5151
5334
|
}
|
|
5152
|
-
// Each removed queued turn's user message was already pushed to the SDK
|
|
5153
|
-
//
|
|
5154
|
-
//
|
|
5335
|
+
// Each removed queued turn's user message was already pushed to the SDK.
|
|
5336
|
+
// It is dropped from Claude Code's queue below (see dropQueuedMessages),
|
|
5337
|
+
// but one Claude Code already dequeued still runs, FIFO, and emits a
|
|
5338
|
+
// result with no user echo to match (0.3.246+ CLIs do stamp results with the
|
|
5155
5339
|
// triggering send's user_message_uuid, which ensureActiveTurn uses as
|
|
5156
5340
|
// an exact join when present). Track those so the consumer skips them
|
|
5157
5341
|
// (see ensureActiveTurn) rather than misattributing them to the head.
|
|
@@ -5214,15 +5398,23 @@ export class ClaudeAcpAgent {
|
|
|
5214
5398
|
}
|
|
5215
5399
|
// A deferred active turn (see Turn.deferredSettle) already has its result
|
|
5216
5400
|
// and is only held open for subagents. A settling turn likewise has its
|
|
5217
|
-
// result but is awaiting the bounded checkpoint preview. Settle
|
|
5218
|
-
// "cancelled" NOW
|
|
5219
|
-
// idle while blocked in that preview, and a held turn
|
|
5220
|
-
//
|
|
5221
|
-
//
|
|
5401
|
+
// result but is awaiting the bounded checkpoint preview. Settle it
|
|
5402
|
+
// "cancelled" NOW where the consumer cannot: it cannot process the
|
|
5403
|
+
// interrupt's trailing idle while blocked in that preview, and a held turn
|
|
5404
|
+
// whose session sits idle gets no trailing idle (the interrupt emits
|
|
5405
|
+
// nothing; with no state events consumed yet, the same is assumed).
|
|
5406
|
+
// A held turn whose followup cycle is live (running, or blocked on a
|
|
5407
|
+
// permission request) is left to that idle, like any live turn: the
|
|
5408
|
+
// interrupt still flushes the cycle's output, which must reach the client
|
|
5409
|
+
// before the turn ends cancelled. The captured outcome preserves usage and
|
|
5410
|
+
// metadata (issue #844), while reporter state makes a late checkpoint
|
|
5411
|
+
// response harmless.
|
|
5222
5412
|
{
|
|
5223
5413
|
const active = session.activeTurn;
|
|
5224
|
-
const
|
|
5225
|
-
if (active &&
|
|
5414
|
+
const followupLive = session.lastSessionState === "running" || session.lastSessionState === "requires_action";
|
|
5415
|
+
if (active &&
|
|
5416
|
+
(active.settling || (isHeldOpen(active) && !followupLive)) &&
|
|
5417
|
+
(active.deferredSettle ?? active.settlingOutcome)) {
|
|
5226
5418
|
session.fileChangeReporter?.finish(active.fileChangeReport, "cancelled");
|
|
5227
5419
|
active.settled = true;
|
|
5228
5420
|
// Mirror settleActive's invariants (it is consumer-scoped and
|
|
@@ -5236,17 +5428,16 @@ export class ClaudeAcpAgent {
|
|
|
5236
5428
|
// text since the last boundary was its followups', and left latched
|
|
5237
5429
|
// it would suppress a following replayed turn's issue-#453 fallback.
|
|
5238
5430
|
session.emittedAssistantText = false;
|
|
5239
|
-
// When the interrupt below pre-empts a live cycle
|
|
5240
|
-
//
|
|
5241
|
-
//
|
|
5242
|
-
//
|
|
5243
|
-
//
|
|
5244
|
-
//
|
|
5245
|
-
//
|
|
5246
|
-
//
|
|
5247
|
-
//
|
|
5248
|
-
//
|
|
5249
|
-
// running transition still in the backlog reads as stale idle and
|
|
5431
|
+
// When the interrupt below pre-empts a live cycle (a settling turn's,
|
|
5432
|
+
// or one no state event reported), it produces a trailer idle with no
|
|
5433
|
+
// counted result; that idle would be un-owed and could lag past the
|
|
5434
|
+
// next prompt's echo — read as the fresh turn ending without a result
|
|
5435
|
+
// (issue #825 false-fail).
|
|
5436
|
+
// Pre-count it unless the session sits idle: there the interrupt
|
|
5437
|
+
// emits nothing, and a debt that never drains would mask one future
|
|
5438
|
+
// #825 detection. (lastSessionState is last-CONSUMED, so both stale
|
|
5439
|
+
// reads exist and both are accepted one-cycle windows: a running
|
|
5440
|
+
// transition still in the backlog reads as stale idle and
|
|
5250
5441
|
// under-counts — that false-fail additionally needs the trailer to
|
|
5251
5442
|
// lag past the next echo — while a cycle already completed into the
|
|
5252
5443
|
// backlog reads as stale non-idle and over-counts, masking one
|
|
@@ -5256,14 +5447,7 @@ export class ClaudeAcpAgent {
|
|
|
5256
5447
|
if (session.lastSessionState !== "idle") {
|
|
5257
5448
|
session.owedTrailingIdles++;
|
|
5258
5449
|
}
|
|
5259
|
-
|
|
5260
|
-
// metadata is its quota breakdown, the counterpart of the usage taken
|
|
5261
|
-
// from it here.
|
|
5262
|
-
active.resolve({
|
|
5263
|
-
stopReason: "cancelled",
|
|
5264
|
-
usage: pendingOutcome.usage,
|
|
5265
|
-
...(pendingOutcome._meta ? { _meta: pendingOutcome._meta } : {}),
|
|
5266
|
-
});
|
|
5450
|
+
active.resolve(cancelledOutcome(session, active));
|
|
5267
5451
|
}
|
|
5268
5452
|
}
|
|
5269
5453
|
// A steered active turn (see Turn.steeredEchoes) settles "cancelled" in the
|
|
@@ -5296,6 +5480,9 @@ export class ClaudeAcpAgent {
|
|
|
5296
5480
|
cancelController.abort();
|
|
5297
5481
|
}, this.forceCancelGraceMs);
|
|
5298
5482
|
}
|
|
5483
|
+
// Before the interrupt: once it stops the running turn, Claude Code starts
|
|
5484
|
+
// the next queued message right away.
|
|
5485
|
+
this.dropQueuedMessages(params.sessionId, session.query, abandoned);
|
|
5299
5486
|
const interrupt = session.query.interrupt();
|
|
5300
5487
|
if (!options.awaitInterrupt) {
|
|
5301
5488
|
// The caller closes the query next, which rejects the pending reply.
|
|
@@ -5392,12 +5579,18 @@ export class ClaudeAcpAgent {
|
|
|
5392
5579
|
session.query.close();
|
|
5393
5580
|
}
|
|
5394
5581
|
/** Cleanly tear down a session: cancel in-flight work, release stream
|
|
5395
|
-
* resources, and remove it from the session map.
|
|
5582
|
+
* resources, and remove it from the session map. Returns once the turns it
|
|
5583
|
+
* cancels have ended, so their end (a v1 `session/prompt` response, a v2
|
|
5584
|
+
* `idle` state) goes out before the caller answers: ACP v2's
|
|
5585
|
+
* `session/close` cancels "as if `session/cancel` had been called", then
|
|
5586
|
+
* frees the session. A wedged consumer holds it at most
|
|
5587
|
+
* {@link TEARDOWN_TURN_END_TIMEOUT_MS}. */
|
|
5396
5588
|
async teardownSession(sessionId) {
|
|
5397
5589
|
const session = this.sessions[sessionId];
|
|
5398
5590
|
if (!session) {
|
|
5399
5591
|
return;
|
|
5400
5592
|
}
|
|
5593
|
+
const turnsEnded = Promise.all((session.turnQueue ?? []).flatMap((turn) => (turn.completion ? [turn.completion] : [])));
|
|
5401
5594
|
try {
|
|
5402
5595
|
await this.cancelTurns({ sessionId }, { awaitInterrupt: false });
|
|
5403
5596
|
}
|
|
@@ -5425,6 +5618,10 @@ export class ClaudeAcpAgent {
|
|
|
5425
5618
|
session.nativeSubagentRuntime?.clear();
|
|
5426
5619
|
session.asyncTaskRuntime?.clear();
|
|
5427
5620
|
delete this.sessions[sessionId];
|
|
5621
|
+
const ended = await raceTimeoutAndAbort(turnsEnded, TEARDOWN_TURN_END_TIMEOUT_MS, new AbortController().signal);
|
|
5622
|
+
if (ended.type === "timeout") {
|
|
5623
|
+
this.logger.error(`Session ${sessionId}: its cancelled turns did not end within ${TEARDOWN_TURN_END_TIMEOUT_MS}ms of the teardown`);
|
|
5624
|
+
}
|
|
5428
5625
|
}
|
|
5429
5626
|
/** Tear down all active sessions. Called when the ACP connection closes. */
|
|
5430
5627
|
async dispose() {
|
|
@@ -5531,13 +5728,15 @@ export class ClaudeAcpAgent {
|
|
|
5531
5728
|
await this.applyConfigOptionValue(params.sessionId, session, params.configId, effectiveValue);
|
|
5532
5729
|
return { configOptions: session.configOptions };
|
|
5533
5730
|
}
|
|
5534
|
-
async replaySessionHistory(sessionId, resumedMessages) {
|
|
5731
|
+
async replaySessionHistory(sessionId, resumedMessages, pending) {
|
|
5535
5732
|
const replayStartedAt = performance.now();
|
|
5536
5733
|
const toolUseCache = {};
|
|
5537
5734
|
const messages = resumedMessages ?? (await getSessionMessages(sessionId));
|
|
5538
5735
|
const historyLoadedAt = performance.now();
|
|
5539
5736
|
this.logger.log(`[session/replay] sessionId=${sessionId} phase=read durationMs=${Math.round(historyLoadedAt - replayStartedAt)} messages=${messages.length}`);
|
|
5540
|
-
|
|
5737
|
+
// A pending replay has no session record yet. It uses the state that the record takes later.
|
|
5738
|
+
const replayState = () => pending?.state ?? this.sessions[sessionId];
|
|
5739
|
+
const session = replayState();
|
|
5541
5740
|
// A replay rebuilds the client view, so its plan goes out again.
|
|
5542
5741
|
if (session?.taskState)
|
|
5543
5742
|
forgetPublishedTaskPlan(session.taskState);
|
|
@@ -5549,7 +5748,12 @@ export class ClaudeAcpAgent {
|
|
|
5549
5748
|
sessionId,
|
|
5550
5749
|
state: session.sessionFailureState,
|
|
5551
5750
|
capabilities: this.clientCapabilities,
|
|
5552
|
-
isCurrent: () =>
|
|
5751
|
+
isCurrent: () => {
|
|
5752
|
+
const live = this.sessions[sessionId];
|
|
5753
|
+
if (!live)
|
|
5754
|
+
return pending !== undefined && !pending.stopped;
|
|
5755
|
+
return live.sessionFailureState === session.sessionFailureState;
|
|
5756
|
+
},
|
|
5553
5757
|
sendUpdate: (notification) => this.client.sessionUpdate(notification),
|
|
5554
5758
|
logger: this.logger,
|
|
5555
5759
|
})
|
|
@@ -5704,7 +5908,7 @@ export class ClaudeAcpAgent {
|
|
|
5704
5908
|
await replayAsyncTasks.taskNotification(notification);
|
|
5705
5909
|
};
|
|
5706
5910
|
const replayMessage = async (message) => {
|
|
5707
|
-
if (isReplayHiddenMetaMessage(message)) {
|
|
5911
|
+
if (pending?.stopped || isReplayHiddenMetaMessage(message)) {
|
|
5708
5912
|
return;
|
|
5709
5913
|
}
|
|
5710
5914
|
if (message.type === "user" &&
|
|
@@ -5718,7 +5922,7 @@ export class ClaudeAcpAgent {
|
|
|
5718
5922
|
// a client-supplied id without an extra getSessionMessages read. Not read
|
|
5719
5923
|
// yet (see Session.messageIdToUuid).
|
|
5720
5924
|
const replayMessageId = messageIdForGrouping(message);
|
|
5721
|
-
const replaySession =
|
|
5925
|
+
const replaySession = replayState();
|
|
5722
5926
|
if (replaySession && replayMessageId && message.uuid) {
|
|
5723
5927
|
replaySession.messageIdToUuid.set(replayMessageId, message.uuid);
|
|
5724
5928
|
}
|
|
@@ -5812,8 +6016,9 @@ export class ClaudeAcpAgent {
|
|
|
5812
6016
|
registerHooks: false,
|
|
5813
6017
|
replay: true,
|
|
5814
6018
|
clientCapabilities: this.clientCapabilities,
|
|
5815
|
-
|
|
5816
|
-
|
|
6019
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
6020
|
+
cwd: replayState()?.cwd,
|
|
6021
|
+
taskState: replayState()?.taskState,
|
|
5817
6022
|
messageId: replayMessageId,
|
|
5818
6023
|
parentToolUseId,
|
|
5819
6024
|
})) {
|
|
@@ -5858,7 +6063,7 @@ export class ClaudeAcpAgent {
|
|
|
5858
6063
|
};
|
|
5859
6064
|
for (const message of messages)
|
|
5860
6065
|
await replayMessage(message);
|
|
5861
|
-
if (nativeReplayEnabled) {
|
|
6066
|
+
if (nativeReplayEnabled && !pending?.stopped) {
|
|
5862
6067
|
// Claude history persists sidechain messages and Agent/Task tool uses,
|
|
5863
6068
|
// but not task_started/task_updated lifecycle frames. Recover terminal
|
|
5864
6069
|
// state from the launch tool_result. Missing results and malformed or
|
|
@@ -5951,8 +6156,10 @@ export class ClaudeAcpAgent {
|
|
|
5951
6156
|
// Do not rely on every ACP client settling requestPermission after the
|
|
5952
6157
|
// cancellation signal. The local race guarantees that Claude's tool call
|
|
5953
6158
|
// is released even when an older or broken client ignores $/cancel_request.
|
|
6159
|
+
// The request goes to a subagent's child session for a tool call of that
|
|
6160
|
+
// subagent, but it blocks the turn of the owner session.
|
|
5954
6161
|
try {
|
|
5955
|
-
return await this.withPendingUserInput(
|
|
6162
|
+
return await this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal));
|
|
5956
6163
|
}
|
|
5957
6164
|
catch (error) {
|
|
5958
6165
|
if (signal.aborted) {
|
|
@@ -6062,7 +6269,7 @@ export class ClaudeAcpAgent {
|
|
|
6062
6269
|
// Like permission requests, the elicitation references this toolUseID, so
|
|
6063
6270
|
// make sure the tool_call has surfaced to the client before we send it.
|
|
6064
6271
|
await this.ensureToolCallEmitted(sessionId, toolName, toolUseID, toolInput, parentToolUseId, signal, permissionSessionId);
|
|
6065
|
-
return this.handleAskUserQuestion(permissionSessionId, toolInput, toolUseID, signal);
|
|
6272
|
+
return this.handleAskUserQuestion(sessionId, permissionSessionId, toolInput, toolUseID, signal);
|
|
6066
6273
|
}
|
|
6067
6274
|
// Do not auto-allow here based on the session's advertised mode. Claude
|
|
6068
6275
|
// Code applies bypassPermissions before invoking canUseTool; a request
|
|
@@ -6078,8 +6285,8 @@ export class ClaudeAcpAgent {
|
|
|
6078
6285
|
const noPersistentRule = matchedAskRule !== undefined || suppressAlwaysAllowRule === true;
|
|
6079
6286
|
const durableChangeSet = normalizeDurablePermissionChangeSet(suggestions, noPersistentRule);
|
|
6080
6287
|
const capabilities = this.toolCallCapabilities;
|
|
6081
|
-
const previewContent = capabilities.diffPatch
|
|
6082
|
-
? await previewPatchContent(toolName, toolInput, session.cwd)
|
|
6288
|
+
const previewContent = capabilities.diffPatch || capabilities.v2
|
|
6289
|
+
? await previewPatchContent(toolName, toolInput, session.cwd, capabilities.v2 ? "v2" : "air")
|
|
6083
6290
|
: undefined;
|
|
6084
6291
|
const presentation = buildClaudePermissionPresentation({
|
|
6085
6292
|
toolName,
|
|
@@ -6216,13 +6423,15 @@ export class ClaudeAcpAgent {
|
|
|
6216
6423
|
* Present the built-in AskUserQuestion tool's questions as an ACP form
|
|
6217
6424
|
* elicitation and return the answers as the tool's `updatedInput`. Called from
|
|
6218
6425
|
* `canUseTool` since that is where the SDK routes the tool's permission check.
|
|
6426
|
+
* The elicitation goes to `requestSessionId`, a subagent's child session for
|
|
6427
|
+
* a question it asks, and blocks the turn of `sessionId`.
|
|
6219
6428
|
*/
|
|
6220
|
-
async handleAskUserQuestion(sessionId, toolInput, toolUseID, signal) {
|
|
6429
|
+
async handleAskUserQuestion(sessionId, requestSessionId, toolInput, toolUseID, signal) {
|
|
6221
6430
|
const questions = extractAskUserQuestions(toolInput);
|
|
6222
6431
|
if (!questions) {
|
|
6223
6432
|
return { behavior: "deny", message: "AskUserQuestion called with no valid questions." };
|
|
6224
6433
|
}
|
|
6225
|
-
const createRequest = askUserQuestionsToCreateRequest(questions,
|
|
6434
|
+
const createRequest = askUserQuestionsToCreateRequest(questions, requestSessionId, toolUseID, this.toolCallCapabilities.air.client);
|
|
6226
6435
|
let response;
|
|
6227
6436
|
try {
|
|
6228
6437
|
response = await this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal));
|
|
@@ -6292,7 +6501,7 @@ export class ClaudeAcpAgent {
|
|
|
6292
6501
|
sessionId,
|
|
6293
6502
|
update: {
|
|
6294
6503
|
sessionUpdate: "available_commands_update",
|
|
6295
|
-
availableCommands: getAvailableSlashCommands(commands, session.terminalSlashCommands),
|
|
6504
|
+
availableCommands: getAvailableSlashCommands(commands, session.terminalSlashCommands, this.toolCallCapabilities.air.client ? session.cwd : undefined, (session.skillPaths ??= new Map())),
|
|
6296
6505
|
},
|
|
6297
6506
|
});
|
|
6298
6507
|
}
|
|
@@ -6400,7 +6609,7 @@ export class ClaudeAcpAgent {
|
|
|
6400
6609
|
const seedEffort = effortPinnedForNewModel
|
|
6401
6610
|
? pinnedEffort
|
|
6402
6611
|
: settingsEffortForModel(mergeEffortSettings(session.settingsManager.getSettings(), session.effortSettingsOverride), newModelInfo, value);
|
|
6403
|
-
session.configOptions = buildConfigOptions(session.modes, session.models, session.modelInfos, seedEffort, {
|
|
6612
|
+
session.configOptions = buildConfigOptions(session.modes, session.models, session.modelInfos, seedEffort, session.agents, session.currentAgent, {
|
|
6404
6613
|
// The toggle follows the newly selected model: it disappears when the
|
|
6405
6614
|
// model lacks fast support and reappears (with the retained user
|
|
6406
6615
|
// intent) when a supporting model is selected again.
|
|
@@ -6479,6 +6688,18 @@ export class ClaudeAcpAgent {
|
|
|
6479
6688
|
if (modelChanged)
|
|
6480
6689
|
this.refreshContextWindowInBackground(sessionId, session);
|
|
6481
6690
|
}
|
|
6691
|
+
else if (configId === AGENT_CONFIG_ID) {
|
|
6692
|
+
// Live agent switch — no subprocess restart needed. Apply the SDK flag
|
|
6693
|
+
// first so a rejected control request leaves both `currentAgent` and the
|
|
6694
|
+
// config option untouched (no UI/SDK desync). Passing `null` clears the
|
|
6695
|
+
// flag layer back to the standard Claude Code agent; the change takes
|
|
6696
|
+
// effect on the next turn (SDK >= 0.3.161).
|
|
6697
|
+
await session.query.applyFlagSettings({
|
|
6698
|
+
agent: value === DEFAULT_AGENT_ID ? null : value,
|
|
6699
|
+
});
|
|
6700
|
+
session.currentAgent = value;
|
|
6701
|
+
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
6702
|
+
}
|
|
6482
6703
|
else if (configId === EFFORT_CONFIG_ID) {
|
|
6483
6704
|
// Apply first so a rejected control request cannot leave the displayed
|
|
6484
6705
|
// value ahead of the SDK flag layer.
|
|
@@ -6613,6 +6834,8 @@ export class ClaudeAcpAgent {
|
|
|
6613
6834
|
}
|
|
6614
6835
|
async getOrCreateSession(params, resumedSession) {
|
|
6615
6836
|
const existingSession = this.sessions[params.sessionId];
|
|
6837
|
+
// A recreated live session keeps its mode, not the mode of the transcript.
|
|
6838
|
+
const livePermissionMode = existingSession?.modes.currentModeId;
|
|
6616
6839
|
if (existingSession) {
|
|
6617
6840
|
const fingerprint = computeSessionFingerprint(params);
|
|
6618
6841
|
if (fingerprint === existingSession.sessionFingerprint) {
|
|
@@ -6628,9 +6851,6 @@ export class ClaudeAcpAgent {
|
|
|
6628
6851
|
// up the new values.
|
|
6629
6852
|
await this.teardownSession(params.sessionId);
|
|
6630
6853
|
}
|
|
6631
|
-
const resumedModelHint = resumedSession
|
|
6632
|
-
? resumedSession.model
|
|
6633
|
-
: await readResumedModel(params.sessionId, this.logger);
|
|
6634
6854
|
const response = await this.createSession({
|
|
6635
6855
|
cwd: params.cwd,
|
|
6636
6856
|
mcpServers: params.mcpServers ?? [],
|
|
@@ -6638,7 +6858,8 @@ export class ClaudeAcpAgent {
|
|
|
6638
6858
|
_meta: params._meta,
|
|
6639
6859
|
}, {
|
|
6640
6860
|
resume: params.sessionId,
|
|
6641
|
-
|
|
6861
|
+
permissionMode: livePermissionMode,
|
|
6862
|
+
...(resumedSession ? { resumedModelHint: resumedSession.model } : {}),
|
|
6642
6863
|
});
|
|
6643
6864
|
return {
|
|
6644
6865
|
sessionId: response.sessionId,
|
|
@@ -6667,6 +6888,12 @@ export class ClaudeAcpAgent {
|
|
|
6667
6888
|
throw RequestError.invalidParams({ cwd }, `\`cwd\` is not a directory: ${cwd}`);
|
|
6668
6889
|
}
|
|
6669
6890
|
}
|
|
6891
|
+
/** Whether the session forwards the text and the thinking of a subagent. */
|
|
6892
|
+
forwardsSubagentText(meta) {
|
|
6893
|
+
return (clientSupportsSubagents(this.clientCapabilities) ||
|
|
6894
|
+
supportsSubagentTranscript(this.clientCapabilities) ||
|
|
6895
|
+
meta?.claudeCode?.options?.forwardSubagentText === true);
|
|
6896
|
+
}
|
|
6670
6897
|
async createSession(params, creationOpts = {}) {
|
|
6671
6898
|
const createStartedAt = performance.now();
|
|
6672
6899
|
// Validate `cwd` up front. The ACP spec requires an absolute path, and the
|
|
@@ -6704,10 +6931,15 @@ export class ClaudeAcpAgent {
|
|
|
6704
6931
|
// sign-out respawn, provider rerouting), so fill the same fast local hint
|
|
6705
6932
|
// here when the caller did not provide it. Never fall back to the slow
|
|
6706
6933
|
// getContextUsage control request.
|
|
6934
|
+
// The same tail read gives the permission mode of the resumed session.
|
|
6707
6935
|
let resumedModelHint = creationOpts.resumedModelHint;
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6936
|
+
let resumedPermissionMode;
|
|
6937
|
+
if (creationOpts.resume !== undefined) {
|
|
6938
|
+
const withModel = !Object.prototype.hasOwnProperty.call(creationOpts, "resumedModelHint");
|
|
6939
|
+
const tail = await readResumedTail(creationOpts.resume, this.logger, withModel);
|
|
6940
|
+
if (withModel)
|
|
6941
|
+
resumedModelHint = tail.model;
|
|
6942
|
+
resumedPermissionMode = tail.permissionMode;
|
|
6711
6943
|
timing.phase("resume-transcript");
|
|
6712
6944
|
}
|
|
6713
6945
|
const input = new Pushable();
|
|
@@ -6769,18 +7001,13 @@ export class ClaudeAcpAgent {
|
|
|
6769
7001
|
const allowBypass = ALLOW_BYPASS &&
|
|
6770
7002
|
settingsManager.getSettings().permissions?.disableBypassPermissionsMode !== "disable" &&
|
|
6771
7003
|
sessionMeta?.claudeCode?.options?.allowDangerouslySkipPermissions !== false;
|
|
6772
|
-
const initialPermissionMode =
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
:
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
if (userProvidedOptions)
|
|
6780
|
-
delete userProvidedOptions.agent;
|
|
6781
|
-
const forwardSubagentText = clientSupportsSubagents(this.clientCapabilities) ||
|
|
6782
|
-
supportsSubagentTranscript(this.clientCapabilities) ||
|
|
6783
|
-
userProvidedOptions?.forwardSubagentText === true;
|
|
7004
|
+
const initialPermissionMode = resolveInitialPermissionMode({
|
|
7005
|
+
explicit: creationOpts.permissionMode,
|
|
7006
|
+
resumed: resumedPermissionMode,
|
|
7007
|
+
defaultMode: settingsManager.getSettings().permissions?.defaultMode,
|
|
7008
|
+
}, this.logger, allowBypass);
|
|
7009
|
+
const userProvidedOptions = sessionMeta?.claudeCode?.options;
|
|
7010
|
+
const forwardSubagentText = this.forwardsSubagentText(params._meta);
|
|
6784
7011
|
// Configure thinking behavior from environment variable
|
|
6785
7012
|
const thinking = resolveThinkingConfig(process.env.MAX_THINKING_TOKENS, this.logger);
|
|
6786
7013
|
// Parse model configuration from environment (e.g. Bedrock model overrides)
|
|
@@ -6806,7 +7033,7 @@ export class ClaudeAcpAgent {
|
|
|
6806
7033
|
// Per-session task state. Created here (rather than in the session record
|
|
6807
7034
|
// below) so the TaskCreated/TaskCompleted hook callbacks can close over
|
|
6808
7035
|
// the same Map that the streaming message handler will read from.
|
|
6809
|
-
const taskState = new Map();
|
|
7036
|
+
const taskState = creationOpts.replayState?.taskState ?? new Map();
|
|
6810
7037
|
// Resolve every workspace root once. The native checkpoint report uses
|
|
6811
7038
|
// this same set for lexical path validation, and the SDK receives it below.
|
|
6812
7039
|
const acpAdditionalDirectories = params.additionalDirectories ?? sessionMeta?.additionalRoots ?? [];
|
|
@@ -7099,7 +7326,7 @@ export class ClaudeAcpAgent {
|
|
|
7099
7326
|
const allowedModels = Array.isArray(settingsAvailableModels)
|
|
7100
7327
|
? applyAvailableModelsAllowlist(initializationResult.models, settingsAvailableModels, settingsModelOverrides, settingsManager.getManagedDeniedModels())
|
|
7101
7328
|
: initializationResult.models;
|
|
7102
|
-
const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, resumedModelHint);
|
|
7329
|
+
const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, await resumedModelHint);
|
|
7103
7330
|
timing.phase("models");
|
|
7104
7331
|
// Resolve the current model's capabilities separately from the stable
|
|
7105
7332
|
// permission-mode catalog advertised to ACP clients.
|
|
@@ -7144,6 +7371,17 @@ export class ClaudeAcpAgent {
|
|
|
7144
7371
|
allowBypass,
|
|
7145
7372
|
});
|
|
7146
7373
|
timing.phase("modes");
|
|
7374
|
+
const agents = await discoverCustomAgents(q);
|
|
7375
|
+
timing.phase("agents");
|
|
7376
|
+
// Only adopt the requested agent as the selected value if it's one we
|
|
7377
|
+
// actually surface in the picker. A built-in (filtered out above) or
|
|
7378
|
+
// otherwise-unknown name would leave the config option's `currentValue`
|
|
7379
|
+
// pointing at an entry not in its own `options` list, which clients render
|
|
7380
|
+
// as a blank/invalid selection.
|
|
7381
|
+
const requestedAgent = userProvidedOptions?.agent;
|
|
7382
|
+
const currentAgent = requestedAgent && agents.some((a) => a.name === requestedAgent)
|
|
7383
|
+
? requestedAgent
|
|
7384
|
+
: DEFAULT_AGENT_ID;
|
|
7147
7385
|
// Seed Fast mode from the SDK's reported state so the UI reflects reality
|
|
7148
7386
|
// (the CLI may start a session with fast mode already on, or force it off
|
|
7149
7387
|
// when `fastModePerSessionOptIn` is set). The toggle is only surfaced while
|
|
@@ -7169,7 +7407,7 @@ export class ClaudeAcpAgent {
|
|
|
7169
7407
|
// Legacy clients still leave effort resolution entirely to the CLI.
|
|
7170
7408
|
const useRecommendedValue = clientSupportsRecommendedConfigValue(this.clientCapabilities);
|
|
7171
7409
|
const effortSettings = mergeEffortSettings(settingsManager.getSettings(), configuredSettingsObject);
|
|
7172
|
-
const configOptions = buildConfigOptions(modes, models, modelInfos, userProvidedOptions?.effort ?? settingsEffortForModel(effortSettings, currentModelInfo), fastMode, {
|
|
7410
|
+
const configOptions = buildConfigOptions(modes, models, modelInfos, userProvidedOptions?.effort ?? settingsEffortForModel(effortSettings, currentModelInfo), agents, currentAgent, fastMode, {
|
|
7173
7411
|
useRecommendedValue,
|
|
7174
7412
|
});
|
|
7175
7413
|
const initialEffort = configOptions.find((option) => option.id === EFFORT_CONFIG_ID);
|
|
@@ -7230,6 +7468,8 @@ export class ClaudeAcpAgent {
|
|
|
7230
7468
|
initialEffort?.currentValue === userProvidedOptions.effort
|
|
7231
7469
|
? userProvidedOptions.effort
|
|
7232
7470
|
: undefined,
|
|
7471
|
+
agents,
|
|
7472
|
+
currentAgent,
|
|
7233
7473
|
fastModeEnabled,
|
|
7234
7474
|
fastModeDisabledReason,
|
|
7235
7475
|
abortController,
|
|
@@ -7248,8 +7488,8 @@ export class ClaudeAcpAgent {
|
|
|
7248
7488
|
nativeSubagentParentByToolUseId: new Map(),
|
|
7249
7489
|
emittedAssistantText: false,
|
|
7250
7490
|
owedTrailingIdles: 0,
|
|
7251
|
-
messageIdToUuid: new Map(),
|
|
7252
|
-
sessionFailureState: createSessionFailureState(),
|
|
7491
|
+
messageIdToUuid: creationOpts.replayState?.messageIdToUuid ?? new Map(),
|
|
7492
|
+
sessionFailureState: creationOpts.replayState?.sessionFailureState ?? createSessionFailureState(),
|
|
7253
7493
|
claudeSubscriptionGuard,
|
|
7254
7494
|
accountKind: fromAccountInfo(initializationResult.account)?.kind,
|
|
7255
7495
|
fileChangeReporter,
|
|
@@ -7290,7 +7530,10 @@ export class ClaudeAcpAgent {
|
|
|
7290
7530
|
this.closeQueryStream(session);
|
|
7291
7531
|
delete this.sessions[sessionId];
|
|
7292
7532
|
try {
|
|
7293
|
-
await this.createSession(session.creationParams, {
|
|
7533
|
+
await this.createSession(session.creationParams, {
|
|
7534
|
+
resume: sessionId,
|
|
7535
|
+
permissionMode: session.modes.currentModeId,
|
|
7536
|
+
});
|
|
7294
7537
|
}
|
|
7295
7538
|
catch (error) {
|
|
7296
7539
|
// One session that cannot come back must not abort the switch. The
|
|
@@ -7384,6 +7627,20 @@ function turnOutcome(session, stopReason, extraMeta) {
|
|
|
7384
7627
|
_meta: { ...turnQuotaMeta(session), ...extraMeta },
|
|
7385
7628
|
};
|
|
7386
7629
|
}
|
|
7630
|
+
/** The outcome of `turn` when a cancel ends it. A turn that already has its
|
|
7631
|
+
* result, because it is held open for its background subagents (see
|
|
7632
|
+
* Turn.deferredSettle) or is settling (see Turn.settlingOutcome), keeps the
|
|
7633
|
+
* usage and metadata that result recorded (issue #844). */
|
|
7634
|
+
function cancelledOutcome(session, turn) {
|
|
7635
|
+
const recorded = turn?.deferredSettle ?? turn?.settlingOutcome;
|
|
7636
|
+
if (!recorded)
|
|
7637
|
+
return turnOutcome(session, "cancelled");
|
|
7638
|
+
return {
|
|
7639
|
+
stopReason: "cancelled",
|
|
7640
|
+
usage: recorded.usage,
|
|
7641
|
+
...(recorded._meta ? { _meta: recorded._meta } : {}),
|
|
7642
|
+
};
|
|
7643
|
+
}
|
|
7387
7644
|
/** `_meta.quota` for a prompt response: what the turn spent, shaped like
|
|
7388
7645
|
* codex-acp's (snake_case container keys, camelCase counters) so a client
|
|
7389
7646
|
* reads one shape from either agent.
|
|
@@ -7645,11 +7902,43 @@ function isValidBaseUrl(baseUrl) {
|
|
|
7645
7902
|
}
|
|
7646
7903
|
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
7647
7904
|
}
|
|
7905
|
+
// `supportedAgents()` always returns Claude Code's built-in subagents — the
|
|
7906
|
+
// ones used for Task-tool delegation (Explore, Plan, etc.) — even when the user
|
|
7907
|
+
// has configured none of their own. Those aren't meaningful *main-thread*
|
|
7908
|
+
// personas, so we filter them out and only surface the Agent picker when the
|
|
7909
|
+
// user (or a plugin/project) has configured custom agents. Update this set if
|
|
7910
|
+
// the SDK's built-in roster changes.
|
|
7911
|
+
export const BUILTIN_AGENT_NAMES = new Set([
|
|
7912
|
+
"claude",
|
|
7913
|
+
"general-purpose",
|
|
7914
|
+
"Explore",
|
|
7915
|
+
"Plan",
|
|
7916
|
+
"statusline-setup",
|
|
7917
|
+
]);
|
|
7918
|
+
// Value of the synthetic "Default" entry in the agent picker, which maps to the
|
|
7919
|
+
// standard Claude Code agent (`applyFlagSettings({ agent: null })`). It is a
|
|
7920
|
+
// reserved sentinel: a custom agent named exactly this would collide with it
|
|
7921
|
+
// (two options sharing the value, selection silently routing to `null`), so we
|
|
7922
|
+
// exclude that name from discovery.
|
|
7923
|
+
/** Discover user/plugin/project-configured main-thread agents, excluding the
|
|
7924
|
+
* built-in subagents and the reserved "default" sentinel. Returns an empty
|
|
7925
|
+
* list if discovery fails so a flaky control request never blocks session
|
|
7926
|
+
* creation. */
|
|
7927
|
+
export async function discoverCustomAgents(q) {
|
|
7928
|
+
try {
|
|
7929
|
+
const agents = await q.supportedAgents();
|
|
7930
|
+
return agents.filter((a) => !BUILTIN_AGENT_NAMES.has(a.name) && a.name !== DEFAULT_AGENT_ID);
|
|
7931
|
+
}
|
|
7932
|
+
catch {
|
|
7933
|
+
return [];
|
|
7934
|
+
}
|
|
7935
|
+
}
|
|
7648
7936
|
/** Stable ids for the session config options surfaced via `configOptions`.
|
|
7649
7937
|
* Centralized so the option declarations in `buildConfigOptions` and the
|
|
7650
7938
|
* handlers in `setSessionConfigOption`/`applyConfigOptionValue` reference the
|
|
7651
7939
|
* same identifiers and can't drift apart. */
|
|
7652
7940
|
export { MODE_CONFIG_ID };
|
|
7941
|
+
export const AGENT_CONFIG_ID = "agent";
|
|
7653
7942
|
export const FAST_MODE_CONFIG_ID = "fast";
|
|
7654
7943
|
/** Select-fallback values used when the client has not opted into boolean
|
|
7655
7944
|
* config options (see {@link createFastModeConfigOption}). */
|
|
@@ -7751,7 +8040,7 @@ export function resolveFastModeEnabled(params) {
|
|
|
7751
8040
|
}
|
|
7752
8041
|
throw new Error(`Invalid value for config option ${FAST_MODE_CONFIG_ID}: ${value}`);
|
|
7753
8042
|
}
|
|
7754
|
-
export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel, fastMode, presentation) {
|
|
8043
|
+
export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel, agents = [], currentAgent = DEFAULT_AGENT_ID, fastMode, presentation) {
|
|
7755
8044
|
const options = [
|
|
7756
8045
|
SessionModeManager.configOption(modes),
|
|
7757
8046
|
buildModelConfigOption(models, modelInfos, presentation?.useRecommendedValue === true),
|
|
@@ -7765,13 +8054,47 @@ export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel
|
|
|
7765
8054
|
if (fastMode?.supported) {
|
|
7766
8055
|
options.push(createFastModeConfigOption(fastMode.enabled, fastMode.useBooleanOption, fastMode.disabledReason));
|
|
7767
8056
|
}
|
|
8057
|
+
// Only surface the Agent picker when there's a real choice — i.e. the user
|
|
8058
|
+
// has configured at least one custom agent (built-ins are filtered out in
|
|
8059
|
+
// discoverCustomAgents). With none configured, "Default" would be the only
|
|
8060
|
+
// entry, so we omit the option entirely.
|
|
8061
|
+
if (agents.length > 0) {
|
|
8062
|
+
options.push({
|
|
8063
|
+
id: AGENT_CONFIG_ID,
|
|
8064
|
+
name: "Agent",
|
|
8065
|
+
description: "Main-thread agent persona",
|
|
8066
|
+
type: "select",
|
|
8067
|
+
currentValue: currentAgent,
|
|
8068
|
+
options: [
|
|
8069
|
+
{ value: DEFAULT_AGENT_ID, name: "Default", description: "Standard Claude Code agent" },
|
|
8070
|
+
...agents.map((a) => ({
|
|
8071
|
+
value: a.name,
|
|
8072
|
+
name: a.name,
|
|
8073
|
+
description: a.description || undefined,
|
|
8074
|
+
})),
|
|
8075
|
+
],
|
|
8076
|
+
});
|
|
8077
|
+
}
|
|
7768
8078
|
return options;
|
|
7769
8079
|
}
|
|
7770
8080
|
function getAvailableSlashCommands(commands,
|
|
7771
8081
|
// Names the CLI tagged terminal-bound on `system`/init (their UX lives in
|
|
7772
8082
|
// the CLI's own terminal, which ACP clients aren't) — filtered alongside
|
|
7773
8083
|
// the static list. Raw CLI names, matched before the MCP rename.
|
|
7774
|
-
terminalCommands
|
|
8084
|
+
terminalCommands,
|
|
8085
|
+
// The session cwd of an AIR client, or undefined for every other client.
|
|
8086
|
+
// AIR gets the kind and the SKILL.md path of each skill command.
|
|
8087
|
+
airSkillCwd,
|
|
8088
|
+
// The resolved SKILL.md paths of the session, keyed by the cwd and the name.
|
|
8089
|
+
skillPaths) {
|
|
8090
|
+
const skillPath = (name, cwd) => {
|
|
8091
|
+
const key = `${cwd}\0${name}`;
|
|
8092
|
+
if (skillPaths?.has(key))
|
|
8093
|
+
return skillPaths.get(key);
|
|
8094
|
+
const resolved = resolveSkillPath(name, cwd);
|
|
8095
|
+
skillPaths?.set(key, resolved);
|
|
8096
|
+
return resolved;
|
|
8097
|
+
};
|
|
7775
8098
|
const UNSUPPORTED_COMMANDS = [
|
|
7776
8099
|
"clear",
|
|
7777
8100
|
"cost",
|
|
@@ -7782,8 +8105,10 @@ terminalCommands) {
|
|
|
7782
8105
|
"release-notes",
|
|
7783
8106
|
"todos",
|
|
7784
8107
|
];
|
|
7785
|
-
|
|
7786
|
-
|
|
8108
|
+
// The adapter replaces the terminal text of `/mcp` and runs its actions,
|
|
8109
|
+
// even when Claude Code tags the command terminal-bound.
|
|
8110
|
+
const advertised = commands
|
|
8111
|
+
.filter((command) => command.name !== "mcp" && !terminalCommands?.includes(command.name))
|
|
7787
8112
|
.map((command) => {
|
|
7788
8113
|
const input = command.argumentHint
|
|
7789
8114
|
? {
|
|
@@ -7793,16 +8118,26 @@ terminalCommands) {
|
|
|
7793
8118
|
}
|
|
7794
8119
|
: null;
|
|
7795
8120
|
let name = command.name;
|
|
7796
|
-
|
|
8121
|
+
const mcpPrompt = command.name.endsWith(" (MCP)");
|
|
8122
|
+
if (mcpPrompt) {
|
|
7797
8123
|
name = `mcp:${name.replace(" (MCP)", "")}`;
|
|
7798
8124
|
}
|
|
8125
|
+
const skillMdPath = airSkillCwd && !command.builtin && !mcpPrompt
|
|
8126
|
+
? skillPath(command.name, airSkillCwd)
|
|
8127
|
+
: undefined;
|
|
7799
8128
|
return {
|
|
7800
8129
|
name,
|
|
7801
8130
|
description: command.description || "",
|
|
7802
8131
|
input,
|
|
8132
|
+
...(skillMdPath
|
|
8133
|
+
? {
|
|
8134
|
+
_meta: withAirMeta(withAirMeta(undefined, AIR_KIND_KEY, "skill"), AIR_SKILL_PATH_KEY, skillMdPath),
|
|
8135
|
+
}
|
|
8136
|
+
: {}),
|
|
7803
8137
|
};
|
|
7804
8138
|
})
|
|
7805
8139
|
.filter((command) => !UNSUPPORTED_COMMANDS.includes(command.name));
|
|
8140
|
+
return [...advertised, MCP_AVAILABLE_COMMAND];
|
|
7806
8141
|
}
|
|
7807
8142
|
function formatUriAsLink(uri) {
|
|
7808
8143
|
try {
|
|
@@ -7977,10 +8312,19 @@ function toolCallFieldsOf(session) {
|
|
|
7977
8312
|
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
7978
8313
|
* Only handles text, image, and thinking chunks for now.
|
|
7979
8314
|
*/
|
|
8315
|
+
/**
|
|
8316
|
+
* The renderer of the notification functions: the agent's tool call
|
|
8317
|
+
* capabilities when they are passed, else those of the ACP capabilities.
|
|
8318
|
+
*/
|
|
8319
|
+
function toolCallRenderer(options) {
|
|
8320
|
+
return options?.toolCallCapabilities
|
|
8321
|
+
? new AcpToolCallRenderer(options.toolCallCapabilities, options.replay)
|
|
8322
|
+
: AcpToolCallRenderer.for(options?.clientCapabilities, options?.replay);
|
|
8323
|
+
}
|
|
7980
8324
|
export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger, options) {
|
|
7981
8325
|
const taskState = options?.taskState ?? new Map();
|
|
7982
8326
|
const registerHooks = options?.registerHooks !== false;
|
|
7983
|
-
const renderer =
|
|
8327
|
+
const renderer = toolCallRenderer(options);
|
|
7984
8328
|
if (typeof content === "string") {
|
|
7985
8329
|
if (content.length === 0) {
|
|
7986
8330
|
return [];
|
|
@@ -8318,6 +8662,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
8318
8662
|
const streamedToolInputs = options?.streamedToolInputs;
|
|
8319
8663
|
const forwardedOptions = {
|
|
8320
8664
|
clientCapabilities: options?.clientCapabilities,
|
|
8665
|
+
toolCallCapabilities: options?.toolCallCapabilities,
|
|
8321
8666
|
parentToolUseId: message.parent_tool_use_id,
|
|
8322
8667
|
cwd: options?.cwd,
|
|
8323
8668
|
taskState: options?.taskState,
|
|
@@ -8381,7 +8726,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
8381
8726
|
// TodoWrite and the Task* tools never surfaced a tool_call to refine.
|
|
8382
8727
|
if (!shouldEmitToolCall(streamedInput.name))
|
|
8383
8728
|
return [];
|
|
8384
|
-
const update =
|
|
8729
|
+
const update = toolCallRenderer(options).partialRefinement(streamedInput, input, options?.cwd);
|
|
8385
8730
|
if (message.parent_tool_use_id)
|
|
8386
8731
|
stampParentToolUseId(update, message.parent_tool_use_id);
|
|
8387
8732
|
applyMessageId(update, options?.messageId);
|