mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47fee47709 | ||
|
|
c95fc884e8 | ||
|
|
5dd8746237 | ||
|
|
0a0468c720 | ||
|
|
e9163f8553 | ||
|
|
61272acb30 | ||
|
|
9616bdac47 | ||
|
|
1fe7ec09a3 | ||
|
|
228f02ecfb | ||
|
|
23626c9a43 | ||
|
|
8f6ebd1d91 | ||
|
|
07911601cc | ||
|
|
a7e6137f68 | ||
|
|
044c43e0c8 | ||
|
|
9013d1d468 | ||
|
|
d5e1b7fa72 | ||
|
|
960f62d765 | ||
|
|
508453c288 | ||
|
|
8dc8c86426 | ||
|
|
b4b7f561e2 | ||
|
|
41cde99fd7 | ||
|
|
5729c471f9 | ||
|
|
a759e64ef6 | ||
|
|
15fdf26fc7 | ||
|
|
9f38cb67df | ||
|
|
744b2d4112 | ||
|
|
2554c7bf98 | ||
|
|
1c80bf8e56 | ||
|
|
ad3b5fe745 | ||
|
|
794aa84684 | ||
|
|
12d34e64e5 | ||
|
|
59a098c2b5 | ||
|
|
c27e6aec90 | ||
|
|
4f273a20d8 | ||
|
|
b3640599ae | ||
|
|
58549ffe6a | ||
|
|
116f06eab1 | ||
|
|
db6eaaf714 | ||
|
|
d58004a348 | ||
|
|
0525e6dacb | ||
|
|
cc2885f6a9 | ||
|
|
1be5ca57ee | ||
|
|
7de5e4bcca |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
with:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.44.0"
|
||||
".": "0.53.0"
|
||||
}
|
||||
|
||||
115
CHANGELOG.md
115
CHANGELOG.md
@@ -1,5 +1,120 @@
|
||||
# Changelog
|
||||
|
||||
## [0.53.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.52.0...v0.53.0) (2026-06-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add ACP logout support ([#816](https://github.com/agentclientprotocol/claude-agent-acp/issues/816)) ([0a0468c](https://github.com/agentclientprotocol/claude-agent-acp/commit/0a0468c72092b7ceccc551b4353dd92f9c22cbb8))
|
||||
* **deps:** bump @anthropic-ai/claude-agent-sdk to 0.3.195 ([#818](https://github.com/agentclientprotocol/claude-agent-acp/issues/818)) ([5dd8746](https://github.com/agentclientprotocol/claude-agent-acp/commit/5dd87462376597eb7b695200f4c716af46e764d6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Emit tool_call before permission request ([#820](https://github.com/agentclientprotocol/claude-agent-acp/issues/820)) ([c95fc88](https://github.com/agentclientprotocol/claude-agent-acp/commit/c95fc884e82e5a297d1b17b8965690260867835b))
|
||||
|
||||
## [0.52.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.51.0...v0.52.0) (2026-06-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add version flag handling ([#813](https://github.com/agentclientprotocol/claude-agent-acp/issues/813)) ([9616bda](https://github.com/agentclientprotocol/claude-agent-acp/commit/9616bdac47505e4a14c36d667fcffc9ae97e1f2a)), closes [#809](https://github.com/agentclientprotocol/claude-agent-acp/issues/809)
|
||||
* **deps-dev:** bump expect-type from 1.3.0 to 1.4.0 in the minor group ([#814](https://github.com/agentclientprotocol/claude-agent-acp/issues/814)) ([61272ac](https://github.com/agentclientprotocol/claude-agent-acp/commit/61272acb30dcafaa2455d334b11ce2ad97339707))
|
||||
* **deps:** Update @anthropic-ai/claude-agent-sdk to 0.3.191 ([#810](https://github.com/agentclientprotocol/claude-agent-acp/issues/810)) ([228f02e](https://github.com/agentclientprotocol/claude-agent-acp/commit/228f02ecfb23be16e59e121c6b42c0f2b2f40a4e))
|
||||
* Push session title updates at turn end ([#812](https://github.com/agentclientprotocol/claude-agent-acp/issues/812)) ([1fe7ec0](https://github.com/agentclientprotocol/claude-agent-acp/commit/1fe7ec09a3a7bcb7501231dae4b0ffe6ef9b70a4))
|
||||
|
||||
## [0.51.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.50.0...v0.51.0) (2026-06-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deps:** bump the minor group with 11 updates ([#807](https://github.com/agentclientprotocol/claude-agent-acp/issues/807)) ([8f6ebd1](https://github.com/agentclientprotocol/claude-agent-acp/commit/8f6ebd1d9198edf723f4c8c1aa2b49b906c46646))
|
||||
|
||||
## [0.50.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.49.0...v0.50.0) (2026-06-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **acp:** Handle ACP request cancellation signals ([#801](https://github.com/agentclientprotocol/claude-agent-acp/issues/801)) ([9013d1d](https://github.com/agentclientprotocol/claude-agent-acp/commit/9013d1d46883a7f3774a63a9dca16c4a0f634a97))
|
||||
* **deps:** bump actions/checkout from 6.0.3 to 7.0.0 ([#803](https://github.com/agentclientprotocol/claude-agent-acp/issues/803)) ([044c43e](https://github.com/agentclientprotocol/claude-agent-acp/commit/044c43e0c894082b9e747c01e9dbde6e21036823))
|
||||
* **deps:** upgrade to @anthropic-ai/claude-agent-sdk@0.3.186 ([#806](https://github.com/agentclientprotocol/claude-agent-acp/issues/806)) ([a7e6137](https://github.com/agentclientprotocol/claude-agent-acp/commit/a7e6137f6877b72b8daa39e74971c3559db8d28f))
|
||||
|
||||
## [0.49.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.48.0...v0.49.0) (2026-06-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to claude-agent-sdk 0.3.185 ([#798](https://github.com/agentclientprotocol/claude-agent-acp/issues/798)) ([8dc8c86](https://github.com/agentclientprotocol/claude-agent-acp/commit/8dc8c864263fa50d03bec7ebff7aee776604cb3d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Deduplicate streamed assistant blocks by content ([#800](https://github.com/agentclientprotocol/claude-agent-acp/issues/800)) ([960f62d](https://github.com/agentclientprotocol/claude-agent-acp/commit/960f62d76582ae5c9c5575ab66974809049ce1d0))
|
||||
* Infer 1M context from model descriptions ([#799](https://github.com/agentclientprotocol/claude-agent-acp/issues/799)) ([508453c](https://github.com/agentclientprotocol/claude-agent-acp/commit/508453c288b4a12701abd507199e7fa0ab172171))
|
||||
|
||||
## [0.48.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.47.0...v0.48.0) (2026-06-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Agent selection dropdown in config options ([#794](https://github.com/agentclientprotocol/claude-agent-acp/issues/794)) ([5729c47](https://github.com/agentclientprotocol/claude-agent-acp/commit/5729c471f92e1d2a191eb2a6d18c2be47821e9ec))
|
||||
* **deps:** bump the minor group with 11 updates ([#787](https://github.com/agentclientprotocol/claude-agent-acp/issues/787)) ([ad3b5fe](https://github.com/agentclientprotocol/claude-agent-acp/commit/ad3b5fe74527f83964695a1e8056d010b981fc79))
|
||||
* Update to claude-agent-sdk 0.3.183 ([#791](https://github.com/agentclientprotocol/claude-agent-acp/issues/791)) ([744b2d4](https://github.com/agentclientprotocol/claude-agent-acp/commit/744b2d41128f67091d4136283594c0db11ea4db5))
|
||||
* Update to new ACP SDK patterns ([#790](https://github.com/agentclientprotocol/claude-agent-acp/issues/790)) ([2554c7b](https://github.com/agentclientprotocol/claude-agent-acp/commit/2554c7bf980472760a6e7810b826f030d2c3af25))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* duplicate assistant text when turn activates mid-message ([#789](https://github.com/agentclientprotocol/claude-agent-acp/issues/789)) ([1c80bf8](https://github.com/agentclientprotocol/claude-agent-acp/commit/1c80bf8e56a9279dc799e7bbdcae87241e99c18b))
|
||||
* Skip empty thinking chunks ([#793](https://github.com/agentclientprotocol/claude-agent-acp/issues/793)) ([15fdf26](https://github.com/agentclientprotocol/claude-agent-acp/commit/15fdf26fc7d3a6c51e89d3e56fc91dd00cb3d7ae))
|
||||
* surface Bash tool image output instead of dropping it ([#617](https://github.com/agentclientprotocol/claude-agent-acp/issues/617)) ([a759e64](https://github.com/agentclientprotocol/claude-agent-acp/commit/a759e64ef6d9b7c5bfe9a6e6b182db835f8ed3b6))
|
||||
|
||||
## [0.47.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.46.0...v0.47.0) (2026-06-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to claude-agent-sdk 0.3.179 ([#783](https://github.com/agentclientprotocol/claude-agent-acp/issues/783)) ([59a098c](https://github.com/agentclientprotocol/claude-agent-acp/commit/59a098c2b530bbae034e9a2dfbd31f8b4ef2a4d0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Duplicate assistant messages in feed ([#785](https://github.com/agentclientprotocol/claude-agent-acp/issues/785)) ([12d34e6](https://github.com/agentclientprotocol/claude-agent-acp/commit/12d34e64e53564602ac1c38a30127e234c5c25ff))
|
||||
|
||||
## [0.46.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.45.1...v0.46.0) (2026-06-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to claude-agent-sdk 0.3.178 ([#777](https://github.com/agentclientprotocol/claude-agent-acp/issues/777)) ([58549ff](https://github.com/agentclientprotocol/claude-agent-acp/commit/58549ffe6a8b02ce59894e567407bd4299c11428))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Better handle out of turn events ([#780](https://github.com/agentclientprotocol/claude-agent-acp/issues/780)) ([4f273a2](https://github.com/agentclientprotocol/claude-agent-acp/commit/4f273a20d870c9c69f71556b8e0519f1de30f285))
|
||||
* Forward option details in elicitation meta ([#779](https://github.com/agentclientprotocol/claude-agent-acp/issues/779)) ([b364059](https://github.com/agentclientprotocol/claude-agent-acp/commit/b3640599ae685beecacd93e012d5bbc9dac716f7)), closes [#764](https://github.com/agentclientprotocol/claude-agent-acp/issues/764)
|
||||
|
||||
## [0.45.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.45.0...v0.45.1) (2026-06-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix terminal error printing as text instead of terminal output ([#776](https://github.com/agentclientprotocol/claude-agent-acp/issues/776)) ([db6eaaf](https://github.com/agentclientprotocol/claude-agent-acp/commit/db6eaaf71484a321e47093ad65bcf8994943cb31))
|
||||
* Scope custom answers per question ([#774](https://github.com/agentclientprotocol/claude-agent-acp/issues/774)) ([d58004a](https://github.com/agentclientprotocol/claude-agent-acp/commit/d58004a34880e0a76833697319eb2a9efa6a43c7))
|
||||
|
||||
## [0.45.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.44.0...v0.45.0) (2026-06-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deps-dev:** bump the minor group with 3 updates ([#763](https://github.com/agentclientprotocol/claude-agent-acp/issues/763)) ([7de5e4b](https://github.com/agentclientprotocol/claude-agent-acp/commit/7de5e4bcca9bfea70593092060f82bc8abe33e0e))
|
||||
* **deps:** bump @anthropic-ai/claude-agent-sdk to 0.3.177 ([#771](https://github.com/agentclientprotocol/claude-agent-acp/issues/771)) ([1be5ca5](https://github.com/agentclientprotocol/claude-agent-acp/commit/1be5ca57ee772fe90e41126365dc4186a18ad257))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* preserve ANTHROPIC_CUSTOM_MODEL_OPTION when availableModels is set ([#768](https://github.com/agentclientprotocol/claude-agent-acp/issues/768)) ([cc2885f](https://github.com/agentclientprotocol/claude-agent-acp/commit/cc2885f6a9993cf61e759c3c770015f94c218627))
|
||||
|
||||
## [0.44.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.43.0...v0.44.0) (2026-06-09)
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export default [
|
||||
"default-case": "error",
|
||||
"prefer-const": "error",
|
||||
"no-var": "error",
|
||||
eqeqeq: ["error", "always"],
|
||||
eqeqeq: ["error", "smart"],
|
||||
curly: ["error", "all"],
|
||||
},
|
||||
},
|
||||
|
||||
789
package-lock.json
generated
789
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.44.0",
|
||||
"version": "0.53.0",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
@@ -59,23 +59,27 @@
|
||||
},
|
||||
"author": "Zed Industries",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.25.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.170",
|
||||
"@agentclientprotocol/sdk": "1.0.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.195",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.103.0",
|
||||
"@anthropic-ai/sdk": "0.106.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@types/node": "25.9.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.61.0",
|
||||
"@typescript-eslint/parser": "8.61.0",
|
||||
"eslint": "10.4.1",
|
||||
"@tsconfig/node22": "22.0.5",
|
||||
"@types/node": "26.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.62.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"eslint": "10.6.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.6.0",
|
||||
"prettier": "3.8.4",
|
||||
"globals": "17.7.0",
|
||||
"prettier": "3.9.1",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "4.1.8"
|
||||
"vitest": "4.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
1691
src/acp-agent.ts
1691
src/acp-agent.ts
File diff suppressed because it is too large
Load Diff
@@ -116,8 +116,42 @@ function questionFieldKey(index: number): string {
|
||||
return `question_${index}`;
|
||||
}
|
||||
|
||||
/** Form-field key for the optional free-text "custom answer" field. */
|
||||
const CUSTOM_ANSWER_FIELD = "customAnswer";
|
||||
/**
|
||||
* Form-field key for the per-question free-text "custom answer" field that sits
|
||||
* alongside `question_<n>`. Mirrors the first-party clients, where every
|
||||
* question carries its own "Other" box rather than one form-level field.
|
||||
*/
|
||||
function questionCustomFieldKey(index: number): string {
|
||||
return `question_${index}_custom`;
|
||||
}
|
||||
|
||||
/**
|
||||
* `_meta` key under which a bridged enum option carries its structured
|
||||
* `description`/`preview`, since ACP's `EnumOption` has no field for either.
|
||||
* Namespaced like the agent's other `_meta` extensions (`_claude/...`).
|
||||
*/
|
||||
const OPTION_META_KEY = "_claude/askUserQuestionOption";
|
||||
|
||||
/** A single option as supplied by the AskUserQuestion tool. */
|
||||
type AskUserQuestionOption = AskUserQuestion["options"][number];
|
||||
|
||||
/**
|
||||
* Build the `_meta` payload that carries an option's structured `description`
|
||||
* and optional `preview` for clients that can render them as distinct fields.
|
||||
* Returns `undefined` when neither is present, so we don't emit an empty `_meta`.
|
||||
*/
|
||||
function askUserQuestionOptionMeta(
|
||||
option: AskUserQuestionOption,
|
||||
): NonNullable<EnumOption["_meta"]> | undefined {
|
||||
const detail: { description?: string; preview?: string } = {};
|
||||
if (option.description) {
|
||||
detail.description = option.description;
|
||||
}
|
||||
if (option.preview) {
|
||||
detail.preview = option.preview;
|
||||
}
|
||||
return Object.keys(detail).length > 0 ? { [OPTION_META_KEY]: detail } : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the AskUserQuestion tool's questions as an ACP form elicitation.
|
||||
@@ -128,10 +162,11 @@ const CUSTOM_ANSWER_FIELD = "customAnswer";
|
||||
* an array with a titled `anyOf` item enum. The enum `const` is always the
|
||||
* option label, since that is what the tool records as the answer.
|
||||
*
|
||||
* A trailing optional free-text field mirrors the CLI's custom-answer box: the
|
||||
* user can type their own answer instead of (or as well as) picking an option.
|
||||
* Nothing is marked required, so the user can also just skip — matching the
|
||||
* built-in tool, which always offers Skip + a free-text box.
|
||||
* Each question is followed by its own optional free-text "custom answer" field
|
||||
* (`question_<n>_custom`), mirroring the CLI's per-question "Other" box: the
|
||||
* user can type their own answer instead of picking an option, scoped to that
|
||||
* specific question. Nothing is marked required, so the user can also just skip
|
||||
* — matching the built-in tool, which always offers Skip + a free-text box.
|
||||
*/
|
||||
export function askUserQuestionsToCreateRequest(
|
||||
questions: AskUserQuestion[],
|
||||
@@ -142,10 +177,25 @@ export function askUserQuestionsToCreateRequest(
|
||||
const properties: Record<string, ElicitationPropertySchema> = {};
|
||||
|
||||
questions.forEach((question, index) => {
|
||||
const options: EnumOption[] = question.options.map((option) => ({
|
||||
const: option.label,
|
||||
title: option.description ? `${option.label} — ${option.description}` : option.label,
|
||||
}));
|
||||
const options: EnumOption[] = question.options.map((option) => {
|
||||
const enumOption: EnumOption = {
|
||||
const: option.label,
|
||||
// `title` stays a flattened "label — description" string so clients that
|
||||
// only read `const`/`title` still surface the description. Clients that
|
||||
// understand the `_meta` below should prefer its structured fields (and
|
||||
// treat `const` as the clean label) rather than parsing this title.
|
||||
title: option.description ? `${option.label} — ${option.description}` : option.label,
|
||||
};
|
||||
// The SDK option carries a `description` (secondary text) and an optional
|
||||
// `preview` (mockups, code snippets, comparisons shown on focus). Neither
|
||||
// has a structural slot in `EnumOption`, so forward them under ACP's
|
||||
// reserved `_meta` extension point for clients that can render them.
|
||||
const meta = askUserQuestionOptionMeta(option);
|
||||
if (meta) {
|
||||
enumOption._meta = meta;
|
||||
}
|
||||
return enumOption;
|
||||
});
|
||||
|
||||
// For a single question the prompt is carried by `message`, so we don't
|
||||
// repeat it in the field description. With multiple questions each field
|
||||
@@ -156,13 +206,13 @@ export function askUserQuestionsToCreateRequest(
|
||||
properties[questionFieldKey(index)] = question.multiSelect
|
||||
? { type: "array", title, description, items: { anyOf: options } }
|
||||
: { type: "string", title, description, oneOf: options };
|
||||
});
|
||||
|
||||
properties[CUSTOM_ANSWER_FIELD] = {
|
||||
type: "string",
|
||||
title: "Other",
|
||||
description: "Type your own answer instead of choosing an option above (optional).",
|
||||
};
|
||||
properties[questionCustomFieldKey(index)] = {
|
||||
type: "string",
|
||||
title: "Other",
|
||||
description: "Type your own answer instead of choosing an option above (optional).",
|
||||
};
|
||||
});
|
||||
|
||||
const requestedSchema: ElicitationSchema = {
|
||||
type: "object",
|
||||
@@ -182,18 +232,18 @@ export function askUserQuestionsToCreateRequest(
|
||||
|
||||
/** Outcome of an AskUserQuestion elicitation, decoupled from any transport. */
|
||||
export type AskUserQuestionOutcome =
|
||||
| { action: "answered"; updatedInput: Record<string, unknown> }
|
||||
| { action: "cancel" };
|
||||
{ action: "answered"; updatedInput: Record<string, unknown> } | { action: "cancel" };
|
||||
|
||||
/**
|
||||
* Fold an ACP elicitation response into the AskUserQuestion tool's input.
|
||||
*
|
||||
* Selected labels are read back from the indexed form fields and written into
|
||||
* `answers` as a `{ [questionText]: label }` map (comma-joining multi-selects)
|
||||
* — the key shape the tool's own `call()` reads. Free text from the custom-
|
||||
* answer field becomes the tool's top-level `response`. Decline yields empty
|
||||
* answers (the model is told the user skipped rather than the turn aborting);
|
||||
* cancel aborts the tool call.
|
||||
* — the key shape the tool's own `call()` reads. A non-empty per-question
|
||||
* custom-answer field (`question_<n>_custom`) takes precedence over that
|
||||
* question's selection, since the user typed their own answer instead of
|
||||
* picking one. Decline yields empty answers (the model is told the user skipped
|
||||
* rather than the turn aborting); cancel aborts the tool call.
|
||||
*/
|
||||
export function applyAskElicitationResponse(
|
||||
response: CreateElicitationResponse,
|
||||
@@ -213,6 +263,14 @@ export function applyAskElicitationResponse(
|
||||
// stay in sync with what the built-in tool's call() expects to read back.
|
||||
const answers: AskUserQuestionOutput["answers"] = {};
|
||||
questions.forEach((question, index) => {
|
||||
// A typed custom answer wins over the selection: the user chose to write
|
||||
// their own answer for this question instead of picking an option.
|
||||
const custom = content[questionCustomFieldKey(index)];
|
||||
if (typeof custom === "string" && custom.trim() !== "") {
|
||||
answers[question.question] = custom.trim();
|
||||
return;
|
||||
}
|
||||
|
||||
const value = content[questionFieldKey(index)];
|
||||
if (value === undefined || value === null) {
|
||||
return;
|
||||
@@ -224,14 +282,7 @@ export function applyAskElicitationResponse(
|
||||
answers[question.question] = text;
|
||||
});
|
||||
|
||||
const updatedInput: Record<string, unknown> = { ...toolInput, answers };
|
||||
const custom = content[CUSTOM_ANSWER_FIELD];
|
||||
if (typeof custom === "string" && custom.trim() !== "") {
|
||||
const response: AskUserQuestionOutput["response"] = custom;
|
||||
updatedInput.response = response;
|
||||
}
|
||||
|
||||
return { action: "answered", updatedInput };
|
||||
return { action: "answered", updatedInput: { ...toolInput, answers } };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
import { resolveSettings } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { claudeCliPath, runAcp } from "./acp-agent.js";
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
|
||||
// `--cli` is checked first so that `--version`/`-v` (and any other flags) are
|
||||
// forwarded to the wrapped native CLI rather than swallowed by our own version
|
||||
// handler below. Our version flag only applies when not delegating.
|
||||
if (process.argv.includes("--cli")) {
|
||||
const { spawn } = await import("node:child_process");
|
||||
const args = process.argv.slice(2).filter((arg) => arg !== "--cli");
|
||||
@@ -33,6 +37,9 @@ if (process.argv.includes("--cli")) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
} else if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
||||
console.log(packageJson.version);
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Apply env vars from the managed-policy tier before any SDK call so the
|
||||
// SDK subprocess inherits them. Going through resolveSettings (vs. a raw
|
||||
|
||||
@@ -2,8 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import type { AgentSideConnection } from "@agentclientprotocol/sdk";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
|
||||
import type { AcpClient, ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
|
||||
|
||||
const { querySpy } = vi.hoisted(() => ({
|
||||
querySpy: vi.fn(),
|
||||
@@ -21,13 +20,13 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
let tempDir: string;
|
||||
let originalClaudeConfigDir: string | undefined;
|
||||
|
||||
function createMockClient(): AgentSideConnection {
|
||||
function createMockClient(): AcpClient {
|
||||
return {
|
||||
sessionUpdate: async () => {},
|
||||
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||
readTextFile: async () => ({ content: "" }),
|
||||
writeTextFile: async () => ({}),
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
}
|
||||
|
||||
function mockQuery() {
|
||||
@@ -700,6 +699,118 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
expect(byValue["claude-opus-4-6[1m]"].name).toBe("claude-opus-4-6[1m]");
|
||||
expect(byValue["claude-opus-4-6"].name).toBe("claude-opus-4-6");
|
||||
});
|
||||
|
||||
it("preserves ANTHROPIC_CUSTOM_MODEL_OPTION even when absent from the allowlist", async () => {
|
||||
// Per the model-config docs, ANTHROPIC_CUSTOM_MODEL_OPTION adds an entry
|
||||
// "without replacing the built-in aliases" and "appears at the bottom of
|
||||
// the /model picker", so it is exempt from the availableModels allowlist
|
||||
// (the same way the Default option is "not affected by availableModels").
|
||||
// ACP must match: a slim alias allowlist must not hide the custom model
|
||||
// row, and it appears last, after the allowlisted entries.
|
||||
// https://code.claude.com/docs/en/model-config#add-a-custom-model-option
|
||||
const originalEnv = process.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
|
||||
process.env.ANTHROPIC_CUSTOM_MODEL_OPTION = "claude-opus-4-8[1m]";
|
||||
try {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
availableModels: ["sonnet", "opus", "haiku"],
|
||||
}),
|
||||
);
|
||||
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
mockQueryWithModels([
|
||||
{ value: "default", displayName: "Default", description: "Default model" },
|
||||
{ value: "sonnet", displayName: "Sonnet", description: "Claude Sonnet 4.6" },
|
||||
{ value: "opus", displayName: "Opus", description: "Claude Opus 4.6" },
|
||||
{ value: "haiku", displayName: "Haiku", description: "Claude Haiku 4.5" },
|
||||
{
|
||||
value: "claude-opus-4-8[1m]",
|
||||
displayName: "Opus 4.8",
|
||||
description: "Claude Opus 4.8",
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
const modelOption = response.configOptions.find((o: any) => o.id === "model");
|
||||
expect(modelOption.options.map((o: any) => o.value)).toEqual([
|
||||
"default",
|
||||
"sonnet",
|
||||
"opus",
|
||||
"haiku",
|
||||
"claude-opus-4-8[1m]",
|
||||
]);
|
||||
const custom = modelOption.options.find((o: any) => o.value === "claude-opus-4-8[1m]");
|
||||
expect(custom.name).toBe("Opus 4.8");
|
||||
expect(custom.description).toBe("Claude Opus 4.8");
|
||||
} finally {
|
||||
if (originalEnv === undefined) {
|
||||
delete process.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
|
||||
} else {
|
||||
process.env.ANTHROPIC_CUSTOM_MODEL_OPTION = originalEnv;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("does not duplicate the custom model option when also in the allowlist", async () => {
|
||||
// If the user lists the custom model's exact ID in availableModels AND it
|
||||
// is set as ANTHROPIC_CUSTOM_MODEL_OPTION, it must appear exactly once.
|
||||
const originalEnv = process.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
|
||||
process.env.ANTHROPIC_CUSTOM_MODEL_OPTION = "claude-opus-4-8[1m]";
|
||||
try {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
availableModels: ["sonnet", "claude-opus-4-8[1m]"],
|
||||
}),
|
||||
);
|
||||
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
mockQueryWithModels([
|
||||
{ value: "default", displayName: "Default", description: "Default model" },
|
||||
{ value: "sonnet", displayName: "Sonnet", description: "Claude Sonnet 4.6" },
|
||||
{
|
||||
value: "claude-opus-4-8[1m]",
|
||||
displayName: "Opus 4.8",
|
||||
description: "Claude Opus 4.8",
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
const modelOption = response.configOptions.find((o: any) => o.id === "model");
|
||||
expect(modelOption.options.map((o: any) => o.value)).toEqual([
|
||||
"default",
|
||||
"sonnet",
|
||||
"claude-opus-4-8[1m]",
|
||||
]);
|
||||
} finally {
|
||||
if (originalEnv === undefined) {
|
||||
delete process.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
|
||||
} else {
|
||||
process.env.ANTHROPIC_CUSTOM_MODEL_OPTION = originalEnv;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves model aliases like opus[1m] to the correct model", async () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { AgentSideConnection, SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import { SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
|
||||
import type { AcpClient, ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
let capturedOptions: Options | undefined;
|
||||
@@ -52,7 +52,7 @@ describe("additionalRoots", () => {
|
||||
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||
readTextFile: async () => ({ content: "" }),
|
||||
writeTextFile: async () => ({}),
|
||||
} as unknown as AgentSideConnection);
|
||||
} as unknown as AcpClient);
|
||||
});
|
||||
|
||||
afterEach(
|
||||
|
||||
42
src/tests/agent-discovery.test.ts
Normal file
42
src/tests/agent-discovery.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { randomUUID } from "crypto";
|
||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { discoverCustomAgents, BUILTIN_AGENT_NAMES } from "../acp-agent.js";
|
||||
|
||||
// `discoverCustomAgents` distinguishes user/plugin-configured agents from
|
||||
// Claude Code's built-in subagents by name (BUILTIN_AGENT_NAMES), since the
|
||||
// SDK's `AgentInfo` carries no built-in/source flag. That hardcoded set drifts
|
||||
// silently if the SDK changes its default roster: a newly added built-in would
|
||||
// leak into the ACP "Agent" picker as if the user had configured it. This
|
||||
// integration test runs the real SDK so the set is flagged the moment it does.
|
||||
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("agent discovery (SDK)", () => {
|
||||
it("BUILTIN_AGENT_NAMES exactly covers the SDK's default agent roster", async () => {
|
||||
const input = new Pushable<SDKUserMessage>();
|
||||
const q = query({
|
||||
prompt: input,
|
||||
options: {
|
||||
systemPrompt: { type: "preset", preset: "claude_code" },
|
||||
sessionId: randomUUID(),
|
||||
// No setting sources → no user/project/local custom agents are loaded,
|
||||
// so `supportedAgents()` reports exactly the intrinsic built-ins
|
||||
// regardless of what the developer has in ~/.claude/agents.
|
||||
settingSources: [],
|
||||
includePartialMessages: true,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const reported = (await q.supportedAgents()).map((a) => a.name).sort();
|
||||
expect(reported).toEqual([...BUILTIN_AGENT_NAMES].sort());
|
||||
|
||||
// With the built-ins being the only agents present, discovery surfaces
|
||||
// nothing — the ACP "Agent" picker is correctly omitted.
|
||||
expect(await discoverCustomAgents(q)).toEqual([]);
|
||||
} finally {
|
||||
input.end();
|
||||
q.return(undefined);
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect, it, Mock, vi, afterEach, beforeEach } from "vitest";
|
||||
import { ClaudeAcpAgent } from "../acp-agent.js";
|
||||
import { AgentSideConnection } from "@agentclientprotocol/sdk";
|
||||
import { AcpClient, ClaudeAcpAgent } from "../acp-agent.js";
|
||||
|
||||
const mockQuery = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
@@ -39,7 +38,7 @@ describe("authorization", () => {
|
||||
async function createAgentMock(): Promise<[ClaudeAcpAgent, Mock]> {
|
||||
const connectionMock = {
|
||||
sessionUpdate: async (_: any) => {},
|
||||
} as AgentSideConnection;
|
||||
} as AcpClient;
|
||||
|
||||
const agent = new ClaudeAcpAgent(connectionMock);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { AgentSideConnection, RequestError, SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import { RequestError, SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
|
||||
import type { AcpClient, ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
@@ -47,13 +47,13 @@ describe("createSession options merging", () => {
|
||||
let agent: ClaudeAcpAgentType;
|
||||
let ClaudeAcpAgent: typeof ClaudeAcpAgentType;
|
||||
|
||||
function createMockClient(): AgentSideConnection {
|
||||
function createMockClient(): AcpClient {
|
||||
return {
|
||||
sessionUpdate: async (_notification: SessionNotification) => {},
|
||||
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||
readTextFile: async () => ({ content: "" }),
|
||||
writeTextFile: async () => ({}),
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -550,14 +550,15 @@ describe("createSession options merging", () => {
|
||||
});
|
||||
|
||||
it("rejects a cwd that points at a file with invalidParams", async () => {
|
||||
const file = path.join(os.tmpdir(), "claude-acp-cwd-is-a-file.txt");
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-acp-cwd-is-a-file-"));
|
||||
const file = path.join(dir, "not-a-directory.txt");
|
||||
fs.writeFileSync(file, "not a directory");
|
||||
try {
|
||||
await expect(agent.newSession({ cwd: file, mcpServers: [] })).rejects.toMatchObject({
|
||||
code: RequestError.invalidParams().code,
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(file, { force: true });
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { CreateElicitationRequest, CreateElicitationResponse } from "@agentclientprotocol/sdk";
|
||||
import type {
|
||||
CreateElicitationRequest,
|
||||
CreateElicitationResponse,
|
||||
EnumOption,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import type { ElicitationRequest } from "@anthropic-ai/claude-agent-sdk";
|
||||
import {
|
||||
applyAskElicitationResponse,
|
||||
@@ -19,14 +23,18 @@ const SESSION_ID = "session-1";
|
||||
*/
|
||||
function mkQuestion(
|
||||
question: string,
|
||||
options: Array<{ label: string; description?: string }>,
|
||||
options: Array<{ label: string; description?: string; preview?: string }>,
|
||||
opts: { header?: string; multiSelect?: boolean } = {},
|
||||
): AskUserQuestion {
|
||||
return {
|
||||
question,
|
||||
header: opts.header ?? "",
|
||||
multiSelect: opts.multiSelect ?? false,
|
||||
options: options.map((o) => ({ label: o.label, description: o.description ?? "" })),
|
||||
options: options.map((o) => ({
|
||||
label: o.label,
|
||||
description: o.description ?? "",
|
||||
...(o.preview ? { preview: o.preview } : {}),
|
||||
})),
|
||||
} as AskUserQuestion;
|
||||
}
|
||||
|
||||
@@ -179,7 +187,14 @@ describe("askUserQuestionsToCreateRequest", () => {
|
||||
title: "Library",
|
||||
description: undefined,
|
||||
oneOf: [
|
||||
{ const: "date-fns", title: "date-fns — Lightweight" },
|
||||
{
|
||||
const: "date-fns",
|
||||
title: "date-fns — Lightweight",
|
||||
// Structured description forwarded under `_meta` so clients can render
|
||||
// it as secondary text instead of parsing it out of the title.
|
||||
_meta: { "_claude/askUserQuestionOption": { description: "Lightweight" } },
|
||||
},
|
||||
// No description → no `_meta` emitted.
|
||||
{ const: "luxon", title: "luxon" },
|
||||
],
|
||||
});
|
||||
@@ -187,8 +202,52 @@ describe("askUserQuestionsToCreateRequest", () => {
|
||||
expect(schema.properties?.["Which library?"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includes an optional free-text custom-answer field", () => {
|
||||
const questions = [mkQuestion("Which?", [{ label: "A" }, { label: "B" }])];
|
||||
it("forwards option description and preview under _meta for rich clients", () => {
|
||||
const questions = [
|
||||
mkQuestion("Which layout?", [
|
||||
{
|
||||
label: "Grid",
|
||||
description: "Cards in a responsive grid",
|
||||
preview: "```\n[ ] [ ] [ ]\n[ ] [ ] [ ]\n```",
|
||||
},
|
||||
{ label: "List", description: "Stacked rows" },
|
||||
{ label: "Plain" },
|
||||
]),
|
||||
];
|
||||
|
||||
const schema = (
|
||||
askUserQuestionsToCreateRequest(questions, SESSION_ID, undefined) as Extract<
|
||||
CreateElicitationRequest,
|
||||
{ mode: "form" }
|
||||
>
|
||||
).requestedSchema;
|
||||
const oneOf = (schema.properties?.["question_0"] as { oneOf: EnumOption[] }).oneOf;
|
||||
|
||||
// Both description and preview travel structurally; the title still flattens
|
||||
// the description for clients that only read const/title.
|
||||
expect(oneOf[0]).toEqual({
|
||||
const: "Grid",
|
||||
title: "Grid — Cards in a responsive grid",
|
||||
_meta: {
|
||||
"_claude/askUserQuestionOption": {
|
||||
description: "Cards in a responsive grid",
|
||||
preview: "```\n[ ] [ ] [ ]\n[ ] [ ] [ ]\n```",
|
||||
},
|
||||
},
|
||||
});
|
||||
// Description only → preview omitted from _meta.
|
||||
expect(oneOf[1]._meta).toEqual({
|
||||
"_claude/askUserQuestionOption": { description: "Stacked rows" },
|
||||
});
|
||||
// Neither → no _meta at all.
|
||||
expect(oneOf[2]._meta).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includes a per-question optional free-text custom-answer field", () => {
|
||||
const questions = [
|
||||
mkQuestion("Which?", [{ label: "A" }, { label: "B" }]),
|
||||
mkQuestion("What else?", [{ label: "C" }, { label: "D" }]),
|
||||
];
|
||||
const schema = (
|
||||
askUserQuestionsToCreateRequest(questions, SESSION_ID, undefined) as Extract<
|
||||
CreateElicitationRequest,
|
||||
@@ -196,7 +255,15 @@ describe("askUserQuestionsToCreateRequest", () => {
|
||||
>
|
||||
).requestedSchema;
|
||||
|
||||
expect(schema.properties?.["customAnswer"]).toMatchObject({ type: "string", title: "Other" });
|
||||
// Each question gets its own "Other" box, not one shared form-level field.
|
||||
expect(schema.properties?.["question_0_custom"]).toMatchObject({
|
||||
type: "string",
|
||||
title: "Other",
|
||||
});
|
||||
expect(schema.properties?.["question_1_custom"]).toMatchObject({
|
||||
type: "string",
|
||||
title: "Other",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds an array property for multi-select questions and includes per-field question text", () => {
|
||||
@@ -250,10 +317,10 @@ describe("applyAskElicitationResponse", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("maps the free-text custom-answer field to the tool's response", () => {
|
||||
it("folds a per-question custom answer into that question's answer", () => {
|
||||
const response = {
|
||||
action: "accept",
|
||||
content: { question_0: "A", customAnswer: "something else entirely" },
|
||||
content: { question_0: "A", question_1_custom: "something else entirely" },
|
||||
} as CreateElicitationResponse;
|
||||
|
||||
expect(applyAskElicitationResponse(response, toolInput, questions)).toEqual({
|
||||
@@ -261,8 +328,23 @@ describe("applyAskElicitationResponse", () => {
|
||||
updatedInput: {
|
||||
questions,
|
||||
metadata: { source: "test" },
|
||||
answers: { "Single?": "A" },
|
||||
response: "something else entirely",
|
||||
answers: { "Single?": "A", "Multi?": "something else entirely" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers a question's custom answer over its selection", () => {
|
||||
const response = {
|
||||
action: "accept",
|
||||
content: { question_0: "A", question_0_custom: " my own take " },
|
||||
} as CreateElicitationResponse;
|
||||
|
||||
expect(applyAskElicitationResponse(response, toolInput, questions)).toEqual({
|
||||
action: "answered",
|
||||
updatedInput: {
|
||||
questions,
|
||||
metadata: { source: "test" },
|
||||
answers: { "Single?": "my own take" },
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -270,7 +352,7 @@ describe("applyAskElicitationResponse", () => {
|
||||
it("ignores empty selections and blank custom answers", () => {
|
||||
const response = {
|
||||
action: "accept",
|
||||
content: { question_1: [], customAnswer: " " },
|
||||
content: { question_1: [], question_0_custom: " " },
|
||||
} as CreateElicitationResponse;
|
||||
|
||||
expect(applyAskElicitationResponse(response, toolInput, questions)).toEqual({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { AgentSideConnection, SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import { SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
|
||||
import type { AcpClient, ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
|
||||
|
||||
const { registerHookCallbackSpy } = vi.hoisted(() => ({
|
||||
registerHookCallbackSpy: vi.fn(),
|
||||
@@ -84,7 +84,7 @@ describe("session config options", () => {
|
||||
let setModelSpy: ReturnType<typeof vi.fn>;
|
||||
let applyFlagSettingsSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
function createMockClient(): AgentSideConnection {
|
||||
function createMockClient(): AcpClient {
|
||||
return {
|
||||
sessionUpdate: async (notification: SessionNotification) => {
|
||||
sessionUpdates.push(notification);
|
||||
@@ -92,7 +92,7 @@ describe("session config options", () => {
|
||||
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||
readTextFile: async () => ({ content: "" }),
|
||||
writeTextFile: async () => ({}),
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
}
|
||||
|
||||
function populateSession() {
|
||||
@@ -113,17 +113,17 @@ describe("session config options", () => {
|
||||
settingsManager: {},
|
||||
modes: structuredClone(MOCK_MODES),
|
||||
models: structuredClone(MOCK_MODELS),
|
||||
modelInfos: MOCK_MODELS.availableModels.map(
|
||||
(m): ModelInfo => ({
|
||||
value: m.modelId,
|
||||
displayName: m.name,
|
||||
description: m.description,
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
}),
|
||||
),
|
||||
modelInfos: MOCK_MODELS.availableModels.map((m): ModelInfo => ({
|
||||
value: m.modelId,
|
||||
displayName: m.name,
|
||||
description: m.description,
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
})),
|
||||
configOptions: structuredClone(MOCK_CONFIG_OPTIONS),
|
||||
contextWindowSize: 200000,
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -216,6 +216,31 @@ describe("session config options", () => {
|
||||
).rejects.toThrow("Invalid value for config option mode: invalid-mode");
|
||||
});
|
||||
|
||||
it("rejects mode and config changes once the query stream has closed (husk session)", async () => {
|
||||
// After an unexpected stream death the session lingers as a husk
|
||||
// (queryClosed=true) so prompt() can answer with a clear error. The
|
||||
// config/mode handlers must do the same rather than calling setModel/
|
||||
// setPermissionMode on the closed query.
|
||||
const session = (agent as unknown as { sessions: Record<string, { queryClosed?: boolean }> })
|
||||
.sessions[SESSION_ID];
|
||||
session.queryClosed = true;
|
||||
|
||||
await expect(
|
||||
agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-6",
|
||||
}),
|
||||
).rejects.toThrow(/start a new session/);
|
||||
await expect(agent.setSessionMode({ sessionId: SESSION_ID, modeId: "plan" })).rejects.toThrow(
|
||||
/start a new session/,
|
||||
);
|
||||
|
||||
// Short-circuited before touching the (closed) query.
|
||||
expect(setModelSpy).not.toHaveBeenCalled();
|
||||
expect(setPermissionModeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("changes mode, sends current_mode_update but not config_option_update", async () => {
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
@@ -1088,6 +1113,10 @@ describe("session config options", () => {
|
||||
],
|
||||
};
|
||||
|
||||
// The tool_call was already surfaced (by the streamed tool_use chunk), so
|
||||
// the permission request won't re-emit one — keep this focused on options.
|
||||
session.emittedToolCalls.add("toolu_1");
|
||||
|
||||
const canUseTool = (agent as any).canUseTool(SESSION_ID);
|
||||
const signal = new AbortController().signal;
|
||||
try {
|
||||
@@ -1119,6 +1148,10 @@ describe("session config options", () => {
|
||||
],
|
||||
};
|
||||
permissionResponse = { outcome: { outcome: "selected", optionId: "auto" } };
|
||||
// The tool_call was already surfaced (by the streamed tool_use chunk), so
|
||||
// the permission request won't re-emit one — the deny path below should
|
||||
// produce no session updates at all.
|
||||
session.emittedToolCalls.add("toolu_2");
|
||||
|
||||
const canUseTool = (agent as any).canUseTool(SESSION_ID);
|
||||
const result = await canUseTool(
|
||||
@@ -1147,6 +1180,10 @@ describe("session config options", () => {
|
||||
],
|
||||
};
|
||||
|
||||
// The tool_call was already surfaced (by the streamed tool_use chunk), so
|
||||
// the permission request won't re-emit one — keep this focused on options.
|
||||
session.emittedToolCalls.add("toolu_3");
|
||||
|
||||
const canUseTool = (agent as any).canUseTool(SESSION_ID);
|
||||
const signal = new AbortController().signal;
|
||||
try {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { randomUUID } from "crypto";
|
||||
import { AgentSideConnection, RequestError, SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import { RequestError, SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import { query, getSessionMessages } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { ClaudeAcpAgent } from "../acp-agent.js";
|
||||
import { AcpClient, ClaudeAcpAgent } from "../acp-agent.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
function createMockClient(): AgentSideConnection {
|
||||
function createMockClient(): AcpClient {
|
||||
return {
|
||||
sessionUpdate: async (_notification: SessionNotification) => {},
|
||||
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||
readTextFile: async () => ({ content: "" }),
|
||||
writeTextFile: async () => ({}),
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
}
|
||||
|
||||
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("session load/resume lifecycle", () => {
|
||||
@@ -165,7 +165,7 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("session load/resume lifecyc
|
||||
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||
readTextFile: async () => ({ content: "" }),
|
||||
writeTextFile: async () => ({}),
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
|
||||
const commandName =
|
||||
"<command-name>/model</command-name>\n <command-message>model</command-message>\n <command-args>opus</command-args>";
|
||||
@@ -274,7 +274,7 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("session load/resume lifecyc
|
||||
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||
readTextFile: async () => ({ content: "" }),
|
||||
writeTextFile: async () => ({}),
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
|
||||
// Exact shape from https://github.com/zed-industries/claude-code-acp/issues/579.
|
||||
const concatenated =
|
||||
|
||||
@@ -7,13 +7,26 @@ import * as os from "node:os";
|
||||
describe("SettingsManager", () => {
|
||||
let tempDir: string;
|
||||
let settingsManager: SettingsManager;
|
||||
let originalConfigDir: string | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "settings-test-"));
|
||||
// Isolate the user tier. resolveSettings reads the user settings.json from
|
||||
// CLAUDE_CONFIG_DIR at call time; without this it falls back to the real
|
||||
// ~/.claude and the developer's own settings (e.g. availableModels) leak
|
||||
// into the merge, making union-based assertions fail on their machine.
|
||||
originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
process.env.CLAUDE_CONFIG_DIR = path.join(tempDir, "user-config");
|
||||
await fs.promises.mkdir(process.env.CLAUDE_CONFIG_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
settingsManager?.dispose();
|
||||
if (originalConfigDir === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = originalConfigDir;
|
||||
}
|
||||
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { AgentSideConnection, ClientCapabilities } from "@agentclientprotocol/sdk";
|
||||
import { ClientCapabilities } from "@agentclientprotocol/sdk";
|
||||
import { ImageBlockParam, ToolResultBlockParam } from "@anthropic-ai/sdk/resources";
|
||||
import {
|
||||
BetaMCPToolResultBlock,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
BetaBashCodeExecutionResultBlock,
|
||||
BetaBashCodeExecutionToolResultBlockParam,
|
||||
} from "@anthropic-ai/sdk/resources/beta.mjs";
|
||||
import { toAcpNotifications, ToolUseCache, Logger } from "../acp-agent.js";
|
||||
import { AcpClient, toAcpNotifications, ToolUseCache, Logger } from "../acp-agent.js";
|
||||
import {
|
||||
toolUpdateFromToolResult,
|
||||
createPostToolUseHook,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from "../tools.js";
|
||||
|
||||
describe("rawOutput in tool call updates", () => {
|
||||
const mockClient = {} as AgentSideConnection;
|
||||
const mockClient = {} as AcpClient;
|
||||
const mockLogger: Logger = { log: () => {}, error: () => {} };
|
||||
|
||||
it("should include rawOutput with string content for tool_result", () => {
|
||||
@@ -425,7 +425,7 @@ describe("rawOutput in tool call updates", () => {
|
||||
});
|
||||
|
||||
describe("Bash terminal output", () => {
|
||||
const mockClient = {} as AgentSideConnection;
|
||||
const mockClient = {} as AcpClient;
|
||||
const mockLogger: Logger = { log: () => {}, error: () => {} };
|
||||
|
||||
const bashToolUse = {
|
||||
@@ -502,6 +502,23 @@ describe("Bash terminal output", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should route failed commands through the terminal when supportsTerminalOutput is true", () => {
|
||||
const toolResult: ToolResultBlockParam = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_bash",
|
||||
content: "some error output",
|
||||
is_error: true,
|
||||
};
|
||||
const update = toolUpdateFromToolResult(toolResult, bashToolUse, true);
|
||||
|
||||
expect(update.content).toEqual([{ type: "terminal", terminalId: "toolu_bash" }]);
|
||||
expect(update._meta).toEqual({
|
||||
terminal_info: { terminal_id: "toolu_bash" },
|
||||
terminal_output: { terminal_id: "toolu_bash", data: "some error output" },
|
||||
terminal_exit: { terminal_id: "toolu_bash", exit_code: 1, signal: null },
|
||||
});
|
||||
});
|
||||
|
||||
it("should fall back to stderr when stdout is empty", () => {
|
||||
const toolResult = makeBashResult("", "some error output", 1);
|
||||
const update = toolUpdateFromToolResult(toolResult, bashToolUse, false);
|
||||
@@ -610,10 +627,25 @@ describe("Bash terminal output", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should use error handler when is_error is true (early return before Bash case)", () => {
|
||||
it("should route is_error through the terminal when supportsTerminalOutput is true", () => {
|
||||
const toolResult = makeStringBashResult("command not found: bad_cmd", true);
|
||||
const update = toolUpdateFromToolResult(toolResult, bashToolUse, true);
|
||||
|
||||
// Failed Bash commands skip the early error return and reach the Bash
|
||||
// case so the client receives terminal output with a non-zero exit code
|
||||
// instead of plain markdown details.
|
||||
expect(update.content).toEqual([{ type: "terminal", terminalId: "toolu_bash" }]);
|
||||
expect(update._meta).toEqual({
|
||||
terminal_info: { terminal_id: "toolu_bash" },
|
||||
terminal_output: { terminal_id: "toolu_bash", data: "command not found: bad_cmd" },
|
||||
terminal_exit: { terminal_id: "toolu_bash", exit_code: 1, signal: null },
|
||||
});
|
||||
});
|
||||
|
||||
it("should use error handler when is_error is true and terminal output is unsupported", () => {
|
||||
const toolResult = makeStringBashResult("command not found: bad_cmd", true);
|
||||
const update = toolUpdateFromToolResult(toolResult, bashToolUse, false);
|
||||
|
||||
// is_error with content hits the early error return at the top of
|
||||
// toolUpdateFromToolResult, before reaching the Bash switch case.
|
||||
// So there's no terminal _meta, just error-formatted content.
|
||||
@@ -670,6 +702,77 @@ describe("Bash terminal output", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with image array tool_result (local Bash image output path)", () => {
|
||||
// The local Bash tool emits image content as
|
||||
// `[{ type: "image", source: { type: "base64", ... } }]` when a
|
||||
// command produces an image (e.g. piping a base64 data URI).
|
||||
const makeImageBashResult = (
|
||||
data: string,
|
||||
media_type: "image/png" | "image/jpeg" | "image/gif" | "image/webp" = "image/png",
|
||||
): ToolResultBlockParam => ({
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_bash",
|
||||
content: [
|
||||
{
|
||||
type: "image",
|
||||
source: { type: "base64", media_type, data },
|
||||
} as ImageBlockParam,
|
||||
],
|
||||
});
|
||||
|
||||
it("should surface image content as ACP image content (terminal disabled)", () => {
|
||||
const toolResult = makeImageBashResult("iVBORw0KGgo=");
|
||||
const update = toolUpdateFromToolResult(toolResult, bashToolUse, false);
|
||||
|
||||
expect(update.content).toEqual([
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "image", data: "iVBORw0KGgo=", mimeType: "image/png" },
|
||||
},
|
||||
]);
|
||||
expect(update._meta).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should bypass terminal _meta even when supportsTerminalOutput is true", () => {
|
||||
// Binary content cannot be streamed through the terminal-output
|
||||
// _meta channel, so the fix returns ACP content directly and skips
|
||||
// the terminal info/output/exit triple.
|
||||
const toolResult = makeImageBashResult("iVBORw0KGgo=", "image/jpeg");
|
||||
const update = toolUpdateFromToolResult(toolResult, bashToolUse, true);
|
||||
|
||||
expect(update.content).toEqual([
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "image", data: "iVBORw0KGgo=", mimeType: "image/jpeg" },
|
||||
},
|
||||
]);
|
||||
expect(update._meta).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should still surface multi-block content with text + image mixed", () => {
|
||||
const toolResult: ToolResultBlockParam = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_bash",
|
||||
content: [
|
||||
{ type: "text", text: "generated:" },
|
||||
{
|
||||
type: "image",
|
||||
source: { type: "base64", media_type: "image/png", data: "AAAA" },
|
||||
} as ImageBlockParam,
|
||||
],
|
||||
};
|
||||
const update = toolUpdateFromToolResult(toolResult, bashToolUse, false);
|
||||
|
||||
expect(update.content).toEqual([
|
||||
{ type: "content", content: { type: "text", text: "generated:" } },
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "image", data: "AAAA", mimeType: "image/png" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("toAcpNotifications with clientCapabilities", () => {
|
||||
@@ -897,7 +1000,7 @@ describe("Bash terminal output", () => {
|
||||
sessionUpdate: async (notification: any) => {
|
||||
hookUpdates.push(notification);
|
||||
},
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
|
||||
// Register hook callback by processing tool_use
|
||||
toAcpNotifications(
|
||||
@@ -976,7 +1079,7 @@ describe("Bash terminal output", () => {
|
||||
sessionUpdate: async (notification: any) => {
|
||||
hookUpdates.push(notification);
|
||||
},
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
|
||||
toAcpNotifications(
|
||||
[
|
||||
@@ -1061,7 +1164,7 @@ describe("Bash terminal output", () => {
|
||||
sessionUpdate: async (notification: any) => {
|
||||
hookUpdates.push(notification);
|
||||
},
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
|
||||
toAcpNotifications(
|
||||
[
|
||||
@@ -1115,7 +1218,7 @@ describe("Bash terminal output", () => {
|
||||
sessionUpdate: async (notification: any) => {
|
||||
hookUpdates.push(notification);
|
||||
},
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
|
||||
toAcpNotifications(
|
||||
[
|
||||
@@ -1191,7 +1294,7 @@ describe("Bash terminal output", () => {
|
||||
sessionUpdate: async (notification: any) => {
|
||||
hookUpdates.push(notification);
|
||||
},
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
|
||||
toAcpNotifications(
|
||||
[
|
||||
@@ -1272,7 +1375,7 @@ describe("Bash terminal output", () => {
|
||||
sessionUpdate: async (notification: any) => {
|
||||
hookUpdates.push(notification);
|
||||
},
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
|
||||
// Step 1: Process tool_use chunk — registers the PostToolUse hook callback
|
||||
const toolUseChunk = {
|
||||
@@ -1372,7 +1475,7 @@ describe("Bash terminal output", () => {
|
||||
sessionUpdate: async (notification: any) => {
|
||||
hookUpdates.push(notification);
|
||||
},
|
||||
} as unknown as AgentSideConnection;
|
||||
} as unknown as AcpClient;
|
||||
|
||||
// Process tool_use (registers hook)
|
||||
toAcpNotifications(
|
||||
@@ -1537,7 +1640,7 @@ describe("planEntries - undefined input regression", () => {
|
||||
});
|
||||
|
||||
describe("toAcpNotifications - TodoWrite with undefined input regression", () => {
|
||||
const mockClient = {} as AgentSideConnection;
|
||||
const mockClient = {} as AcpClient;
|
||||
const mockLogger: Logger = { log: () => {}, error: () => {} };
|
||||
|
||||
it("should not throw when TodoWrite tool_use has undefined input", () => {
|
||||
@@ -1672,7 +1775,7 @@ describe("applyTaskCreate / applyTaskUpdate", () => {
|
||||
});
|
||||
|
||||
describe("toAcpNotifications - Task* tools", () => {
|
||||
const mockClient = {} as AgentSideConnection;
|
||||
const mockClient = {} as AcpClient;
|
||||
const mockLogger: Logger = { log: () => {}, error: () => {} };
|
||||
|
||||
it("suppresses tool_call for TaskCreate/TaskUpdate/TaskList/TaskGet on tool_use", () => {
|
||||
|
||||
28
src/tools.ts
28
src/tools.ts
@@ -485,7 +485,8 @@ export function toolUpdateFromToolResult(
|
||||
"is_error" in toolResult &&
|
||||
toolResult.is_error &&
|
||||
toolResult.content &&
|
||||
toolResult.content.length > 0
|
||||
toolResult.content.length > 0 &&
|
||||
!(toolUse?.name === "Bash" && supportsTerminalOutput)
|
||||
) {
|
||||
// Only return errors
|
||||
return toAcpContentUpdate(toolResult.content, true);
|
||||
@@ -529,7 +530,9 @@ export function toolUpdateFromToolResult(
|
||||
// Extract output and exit code from either format:
|
||||
// 1. BetaBashCodeExecutionResultBlock: { type: "bash_code_execution_result", stdout, stderr, return_code }
|
||||
// 2. Plain string content from a regular tool_result
|
||||
// 3. Array content (e.g. [{ type: "text", text: "..." }])
|
||||
// 3. Array content (e.g. [{ type: "text", text: "..." }] for stdout,
|
||||
// or [{ type: "image", source: {...} }] when the local Bash tool
|
||||
// produces an image, e.g. piping a base64 data URI)
|
||||
let output = "";
|
||||
let exitCode = isError ? 1 : 0;
|
||||
|
||||
@@ -544,13 +547,20 @@ export function toolUpdateFromToolResult(
|
||||
exitCode = bashResult.return_code;
|
||||
} else if (typeof result === "string") {
|
||||
output = result;
|
||||
} else if (
|
||||
Array.isArray(result) &&
|
||||
result.length > 0 &&
|
||||
"text" in result[0] &&
|
||||
typeof result[0].text === "string"
|
||||
) {
|
||||
output = result.map((c: any) => c.text).join("\n");
|
||||
} else if (Array.isArray(result) && result.length > 0) {
|
||||
const textOnly = result.every(
|
||||
(c: any) => c && typeof c === "object" && typeof c.text === "string",
|
||||
);
|
||||
if (textOnly) {
|
||||
output = result.map((c: any) => c.text).join("\n");
|
||||
} else {
|
||||
// Image (or mixed non-text) content. Binary payloads can't be
|
||||
// streamed through the terminal-output _meta channel, so bypass
|
||||
// it and surface the blocks as ACP content. This handles the
|
||||
// local Bash tool's image output, which previously failed the
|
||||
// text-only guard and was silently dropped.
|
||||
return toAcpContentUpdate(result, isError);
|
||||
}
|
||||
}
|
||||
|
||||
if (supportsTerminalOutput) {
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2021"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
|
||||
Reference in New Issue
Block a user