Compare commits

..

5 Commits

Author SHA1 Message Date
acp-release-bot[bot]
a1d83c6031 chore(main): release 0.103.0 (#1559)
🤖 I have created a release *beep* *boop*
---


##
[0.103.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.102.0...v0.103.0)
(2026-11-10)


### ⚠ BREAKING CHANGES

* deep imports of `dist/` files fail with
`ERR_PACKAGE_PATH_NOT_EXPORTED`, and `lib.ts` exports only `serveAcp`,
`AUTH_STATUS_UPDATE_METHOD` and their types.

### Features

* serve ACP v2 with a direct v2 agent and remove the v1 bridge
([#1560](https://github.com/agentclientprotocol/claude-agent-acp/issues/1560))
([cb285cd](cb285cd285))


### Bug Fixes

* give subagent checklists their own plan ID
([#1558](https://github.com/agentclientprotocol/claude-agent-acp/issues/1558))
([2f7e386](2f7e386f36))


### Code Refactoring

* export only lib.ts, with serveAcp and the authStatus wire names
([#1566](https://github.com/agentclientprotocol/claude-agent-acp/issues/1566))
([566e265](566e2652a9))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
2026-11-10 09:35:53 +01:00
Ben Brandt
966aee0f04 refactor: move the test-only tool render and metadata wrappers into src/tests/ (#1569)
`toolInfoFromToolUse` and `toolUpdateFromToolResult` in `src/tools.ts`
and `isLocalCommandMetadata` in `src/message-filters.ts` were public
`lib.ts` exports. #1566 removed them from `lib.ts`, so now only tests
call them. They're thin wrappers: the first two build an
`AcpToolCallRenderer` for one tool use or result, and the third checks
that `stripLocalCommandMetadata` drops everything.

They move unchanged to `src/tests/tool-render.ts`. `tools.test.ts`,
`diff-patch.test.ts` and `v1/acp-agent.test.ts` import them from there.
Production code renders through `AcpToolCallRenderer` directly, as
before. Code changes only: no test changes and no behavior change.

A scan of `src/` for exports finds no export that nothing uses. The only
remaining test-only exports are the deliberate test hooks
`clearDiffMemo` (`diff.ts`) and `sleep` (`utils.ts`).

Test results:
- `npx tsc --noEmit`, `npm run check`, `npm run build`: pass
- `npx vitest run`: 3939 passed / 33 skipped
2026-11-10 08:31:08 +00:00
Ben Brandt
66adc1a4c9 refactor: share the subagent replay's transcript scan between v1 and v2 (#1568)
`src/subagent-replay.ts` (v1, `RfdSubagentReplay`) and
`src/v2/subagent-replay.ts` (`SubagentReplay`) each kept a copy of the
transcript scan. The copies differed only in formatting and variable
names. The scan does four things:
- It registers each Agent or Task launch that has metadata as a child,
and records a launch without metadata as omitted.
- It records each SendMessage and SubagentHandback call.
- It marks a failed hand-back.
- It sets a foreground launch's end from its tool result.

That scan, the `ReplayChild` and `SendMessageCall` types, the child's
end state, and the `contentBlocks`, `inputOf`, `textOf` and
`isDirectory` helpers move to `ReplayTranscript` in
`subagent-replay.ts`. Each replay holds one and reads `children`,
`sendMessages`, `handbacks` and `omitted` from it.

What each replay sends stays where it was. v1 publishes asynchronously
through `Publish`, and v2 sends synchronously through `SessionOutput`,
with the v2-only rules: each child's end also goes on the child's own
stream, and live children get their current state after the response.
Sharing those parts would tie the two protocols together, so I left them
separate. The scan only parses transcripts, so sharing it adds no
dependency between v1 and v2.

v2's `ReplayChild` is the shared type plus `output?: SessionOutput`. Its
`launchEnd` was a narrower `"completed" | "failed" | "cancelled"`, which
`endState` passed through `nativeSubagentState`. That call returns those
three values unchanged, so `replayEndState` gives the same result in
both versions.

No behavior change: 128 lines added and 242 removed, with no test
changes.

Test results:
- `npx tsc --noEmit`, `npm run check`, `npm run build`: pass
- `npx vitest run`: 3939 passed / 33 skipped. The v1 and v2 replay
scenario goldens are unchanged.
- `RUN_INTEGRATION_TESTS=1 npx vitest run`: passes. One run hit the
known load flake "absorbs the interrupt's lagged trailer after
cancelling a held turn", which passes on its own.
2026-11-10 08:19:11 +00:00
Ben Brandt
bc031f362c test: split src/tests/ into v1/, v2/ and shared tests (#1567)
`src/tests/` was flat, so it didn't follow the `src/v1/` / `src/v2/`
layout.

- **`src/tests/v1/`** holds the 37 tests that import `src/v1/`, plus
`session-doubles.ts`, which builds v1 sessions.
- **`src/tests/v2/`** holds the tests of `src/v2/` and `serve.ts`:
`v2.test.ts`, `v2-integration.test.ts`, `acp-v2.test.ts` and
`acp-scenarios-v2.test.ts`.
- **The top level** keeps the tests of shared modules, plus
`helpers.ts`, `setup-no-network.ts`, the scenario harness
(`acp-scenarios/`) and `fixtures/`.

A test goes to `v1/` when it imports the v1 agent, even if it is mostly
about a shared module. For example, `providers.test.ts` and
`fast-mode-config.test.ts` drive `ClaudeAcpAgent` for some of their
cases.

Only paths change. Imports and `vi.mock`/`vi.importActual` specifiers
gain one `../`. The `here`-relative paths to the goldens, the
origin/main recordings, `package.json` and `fixtures/` were updated too.
`AGENTS.md`, `docs/air-extensions.md` and the scenario files' doc
comments now give the new paths. No test, golden file or recording
changed. A script checked that every relative module specifier under
`src/tests/` resolves, because a `vi.mock` path that doesn't resolve
mocks nothing and raises no error.

Test results:
- `npx tsc --noEmit`, `npm run check`, `npm run build`: pass
- `npx vitest run`: 3939 passed / 33 skipped, the same as before the
move.
- `RUN_INTEGRATION_TESTS=1 npx vitest run`: 3964 passed / 8 skipped.
2026-11-10 09:07:09 +01:00
Ben Brandt
566e2652a9 refactor!: export only lib.ts, with serveAcp and the authStatus wire names (#1566)
Stacked on #1564.

The package's `exports` map listed `"./*": "./*"`, so every file under
`dist/` was public, and any file move could break a deep import. It now
lists only `.` (`dist/lib.js`) and `./package.json`.

**`lib.ts` exports:**
- `serveAcp`, which serves ACP v1 and v2 on stdio through the SDK's
protocol router. This is what `index.ts` runs.
- The `AcpProtocols` and `Logger` types.
- `AUTH_STATUS_UPDATE_METHOD` and the `authStatus` payload types, which
a client needs to read the extension's traffic.

**No longer exported:**
- `ClaudeAcpAgent`
- `runAcp`: v1 only and unused, replaced by `serveAcp`, and now deleted.
- `toAcpNotifications`, `streamEventToAcpNotifications`,
`isLocalCommandMetadata`, `stripLocalCommandMetadata`
- `toolInfoFromToolUse`, `toolUpdateFromToolResult`, `toDisplayPath`,
`planEntries`
- `SettingsManager`
- `nodeToWebReadable`, `nodeToWebWritable`, `Pushable`, `unreachable`
- The types `NewSessionMeta`, `SDKMessageFilter`, `ToolUpdateMeta` and
`ClaudePlanEntry`.

`src/acp-agent.ts`, which re-exported `src/v1/acp-agent.ts` for the
`dist/acp-agent.js` path, is gone. So are the re-exports in
`src/v1/acp-agent.ts` that kept moved names on that path. Tests now
import those names from their own modules. The ESLint boundary rules and
`AGENTS.md` drop their `acp-agent.ts` exemptions.

`release-please-config.json` sets `bump-minor-pre-major`, so the `!`
bumps the minor version, the same as a `feat:`.

BREAKING CHANGE: deep imports of `dist/` files fail with
`ERR_PACKAGE_PATH_NOT_EXPORTED`, and `lib.ts` exports only `serveAcp`,
`AUTH_STATUS_UPDATE_METHOD` and their types.

Test results:
- `npx tsc --noEmit`, `npm run check`, `npm run build`: pass
- `npx vitest run`: 3939 passed / 33 skipped. `module-exports.test.ts`
pins the `lib.ts` names and the `exports` map.
- `node scripts/smoke-binary.mjs -- node dist/index.js`: pass
- Resolving the package from its root:
`import("@agentclientprotocol/claude-agent-acp")` gives `serveAcp` and
`AUTH_STATUS_UPDATE_METHOD`, `…/dist/acp-agent.js` throws
`ERR_PACKAGE_PATH_NOT_EXPORTED`, and `…/package.json` resolves.
2026-11-10 07:56:24 +00:00
59 changed files with 544 additions and 754 deletions

View File

@@ -1,3 +1,3 @@
{
".": "0.102.0"
".": "0.103.0"
}

View File

@@ -3,10 +3,9 @@
An ACP-compatible coding agent powered by the Claude Agent SDK. Source in `src/`,
TypeScript, ESM, Node >= 22. `src/v1/` holds the v1 agent (`ClaudeAcpAgent` in
`src/v1/acp-agent.ts`), `src/v2/` the v2 agent, and the rest of `src/` the modules
both share. `src/acp-agent.ts` only re-exports `src/v1/acp-agent.ts` for the
published `dist/acp-agent.js` path. ESLint rejects imports between `src/v1/` and
both share. The package exports only `src/lib.ts`. ESLint rejects imports between `src/v1/` and
`src/v2/`, and from shared modules into either agent. The entry points (`index.ts`, `lib.ts`,
`serve.ts`, `binary-entry.ts`, `acp-agent.ts`) are exempt.
`serve.ts`, `binary-entry.ts`) are exempt.
## Commands
@@ -86,9 +85,13 @@ opening a PR.
## Testing guidelines
- Before adding a test, search `src/tests/` for existing tests of the same API
and scenario, and add to their file and `describe` block. Use the helpers in
`helpers.ts` and `session-doubles.ts` instead of copying setup.
- Tests live in `src/tests/`: `v1/` holds the tests that drive the v1 agent,
`v2/` the tests of the v2 agent, and the top level the tests of shared
modules, with `helpers.ts`, the scenario harness (`acp-scenarios/`) and
`fixtures/`. Before adding a test, search `src/tests/` for existing tests of
the same API and scenario, and add to their file and `describe` block. Use
the helpers in `helpers.ts` and `v1/session-doubles.ts` instead of copying
setup.
- Don't add cases that take the same code path. Tests that differ only in input
and expected output belong in one `it.each`. Don't repeat another test's
assertions; assert only what this test is about.

View File

@@ -1,5 +1,26 @@
# Changelog
## [0.103.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.102.0...v0.103.0) (2026-11-10)
### ⚠ BREAKING CHANGES
* deep imports of `dist/` files fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`, and `lib.ts` exports only `serveAcp`, `AUTH_STATUS_UPDATE_METHOD` and their types.
### Features
* serve ACP v2 with a direct v2 agent and remove the v1 bridge ([#1560](https://github.com/agentclientprotocol/claude-agent-acp/issues/1560)) ([cb285cd](https://github.com/agentclientprotocol/claude-agent-acp/commit/cb285cd2857924c07038c129cc836da007e2737f))
### Bug Fixes
* give subagent checklists their own plan ID ([#1558](https://github.com/agentclientprotocol/claude-agent-acp/issues/1558)) ([2f7e386](https://github.com/agentclientprotocol/claude-agent-acp/commit/2f7e386f3600658d905906f3b37301120b07605f))
### Code Refactoring
* export only lib.ts, with serveAcp and the authStatus wire names ([#1566](https://github.com/agentclientprotocol/claude-agent-acp/issues/1566)) ([566e265](https://github.com/agentclientprotocol/claude-agent-acp/commit/566e2652a9da5a31dab57a23ea1b06e9fff2ead8))
## [0.102.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.101.1...v0.102.0) (2026-11-09)

View File

@@ -78,7 +78,7 @@ A page with more sessions after it has a `nextCursor`, as ACP defines.
A client that does not follow `nextCursor` sees only the newest 1000 sessions of the directory.
An unknown cursor gives an `invalidParams` error.
`src/tests/acp-scenarios.test.ts` compares the traffic of a plain client and of Zed with recordings of origin/main.
`src/tests/v1/acp-scenarios.test.ts` compares the traffic of a plain client and of Zed with recordings of origin/main.
## Negotiation
@@ -557,7 +557,7 @@ AIR puts the hooks into `_meta.claudeCode.options.settings.hooks` of `session/ne
```
- The settings go to the flag settings tier of that session only. Other sessions and the settings files do not change.
- Claude Code runs the hooks of every settings tier, so the user, project and IDE hooks of one event all run. `src/tests/session-hooks.test.ts` checks this with the Claude Code binary.
- Claude Code runs the hooks of every settings tier, so the user, project and IDE hooks of one event all run. `src/tests/v1/session-hooks.test.ts` checks this with the Claude Code binary.
- `CLAUDE_MODEL_CONFIG` stays in effect (see [Model configuration](model-configuration.md#precedence)).
- A `session/load` with different settings recreates the query, so it applies the new hooks.
- Claude asks for no hook trust step.

View File

@@ -61,11 +61,11 @@ export default [
curly: ["error", "all"],
},
},
// `src/v2/` must not import the v1 agent (`src/v1/`, or `src/acp-agent.ts`, which re-exports it).
// `src/v2/` must not import the v1 agent.
{
files: ["src/v2/**/*.ts"],
rules: protocolBoundary(
"^(\\.\\./)+(v1/|acp-agent\\.js$)",
"^(\\.\\./)+v1/",
"src/v2/ must not import the v1 agent. Move what both versions need into a shared module in src/.",
),
},
@@ -88,11 +88,10 @@ export default [
"src/lib.ts",
"src/serve.ts",
"src/binary-entry.ts",
"src/acp-agent.ts",
],
rules: protocolBoundary(
"^(\\./|(\\.\\./)+)(v1/|v2/|acp-agent\\.js$)",
"Shared modules must not import src/v1/, src/v2/ or src/acp-agent.ts. Move the code into the shared module, or pass it in from the agent.",
"^(\\./|(\\.\\./)+)(v1|v2)/",
"Shared modules must not import src/v1/ or src/v2/. Move the code into the shared module, or pass it in from the agent.",
),
},
prettierConfig,

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.102.0",
"version": "0.103.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.102.0",
"version": "0.103.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "2.5.0",

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.102.0",
"version": "0.103.0",
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -16,7 +16,7 @@
"types": "./dist/lib.d.ts",
"import": "./dist/lib.js"
},
"./*": "./*"
"./package.json": "./package.json"
},
"files": [
"dist/",

View File

@@ -1,3 +0,0 @@
// The v1 agent lives in `v1/acp-agent.ts`. This module keeps the published
// `dist/acp-agent.js` path that consumers deep-import.
export * from "./v1/acp-agent.js";

View File

@@ -1,19 +1,7 @@
// Export the main agent class and utilities for library usage
export { ClaudeAcpAgent, runAcp } from "./v1/acp-agent.js";
export { isLocalCommandMetadata, stripLocalCommandMetadata } from "./message-filters.js";
export { toAcpNotifications, streamEventToAcpNotifications } from "./sdk-to-acp.js";
export type { NewSessionMeta, SDKMessageFilter } from "./new-session-meta.js";
export type { ToolUpdateMeta } from "./tool-calls/renderer.js";
export { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
export {
toolInfoFromToolUse,
toDisplayPath,
planEntries,
toolUpdateFromToolResult,
} from "./tools.js";
export { SettingsManager, type SettingsManagerOptions } from "./settings.js";
// The `authStatus` extension's wire surface: the notification's method name and
// the payload types needed to read the traffic. The mappers stay internal.
// The package's public API: serve ACP (v1 and v2) on stdio, and the wire
// names of the `authStatus` extension that a client needs to read its traffic.
export { serveAcp, type AcpProtocols } from "./serve.js";
export type { Logger } from "./logger.js";
export {
AUTH_STATUS_UPDATE_METHOD,
type AuthStatus,
@@ -21,6 +9,3 @@ export {
type AuthStatusKind,
type AuthStatusUpdateNotification,
} from "./auth-status.js";
// Export types
export type { ClaudePlanEntry } from "./tools.js";

View File

@@ -139,10 +139,6 @@ export function stripLocalCommandMetadata(content: unknown): unknown | null {
return kept;
}
export function isLocalCommandMetadata(content: unknown): boolean {
return stripLocalCommandMetadata(content) === null;
}
/**
* True for the synthetic assistant message the CLI injects into the transcript
* when a turn fails authentication (e.g. "Not logged in · Please run /login",

View File

@@ -84,7 +84,7 @@ export type ReplayChild = {
responseModel?: string;
};
type SendMessageCall = {
export type SendMessageCall = {
toolUseId: string;
to: string;
message: string;
@@ -97,34 +97,27 @@ type SendMessageCall = {
outgoingSent: boolean;
};
export class RfdSubagentReplay {
/** The children by the id of the tool use that launched each. */
private readonly children = new Map<string, ReplayChild>();
private readonly byAgentId = new Map<string, ReplayChild>();
private readonly sendMessages: SendMessageCall[] = [];
/** Launches without metadata, which the replay leaves out. */
private readonly omitted = new Set<string>();
/** Children whose transcript could not be read: only their association is replayed. */
private withoutHistory = 0;
/** The SubagentHandback calls, with their report and whether their result failed. */
private readonly handbacks = new Map<
string,
{ message: string; failed: boolean; sent: boolean }
>();
/** A SubagentHandback call: its report, whether its result failed, and whether the replay sent it. */
type ReplayHandback = { message: string; failed: boolean; sent: boolean };
constructor(
private readonly history: SubagentHistory,
private readonly publish: Publish,
private readonly models?: SubagentModels,
/**
* Whether the replay maps a model to the session's model list as it goes.
* A load lacks the list until the CLI started, so it maps only in
* {@link currentStates}, after the load response.
*/
private readonly mapsDuringReplay = true,
/** The working directory of the root session, which a child inherits unless it reports its own. */
private readonly rootCwd?: string,
) {}
/**
* What both replays read from the transcripts before they send anything: the
* children that launches with metadata started, the launches without metadata
* (left out), the SendMessage calls, and the SubagentHandback calls. It sends
* nothing; `RfdSubagentReplay` (v1) and `src/v2/subagent-replay.ts` send the
* replay from it.
*/
export class ReplayTranscript<Child extends ReplayChild = ReplayChild> {
/** The children by the id of the tool use that launched each. */
readonly children = new Map<string, Child>();
readonly byAgentId = new Map<string, Child>();
readonly sendMessages: SendMessageCall[] = [];
/** Launches without metadata, which the replay leaves out. */
readonly omitted = new Set<string>();
/** The SubagentHandback calls, by tool use id. */
readonly handbacks = new Map<string, ReplayHandback>();
constructor(private readonly history: SubagentHistory) {}
/**
* Registers the launches, the launch results, and the SendMessage calls in
@@ -165,9 +158,64 @@ export class RfdSubagentReplay {
}
}
private registerLaunch(
toolUseId: string,
input: Record<string, unknown>,
parentSessionId: string,
): void {
if (this.children.has(toolUseId)) return;
const record = this.history.agents.get(toolUseId);
if (!record) {
this.omitted.add(toolUseId);
return;
}
// A child type adds only optional fields, which a new child leaves unset.
const child = {
launchId: toolUseId,
sessionId: record.agentId,
record,
parentSessionId,
input,
announced: false,
promptSent: false,
} as Child;
this.children.set(toolUseId, child);
this.byAgentId.set(record.agentId, child);
}
}
export class RfdSubagentReplay {
private readonly transcript: ReplayTranscript;
/** Children whose transcript could not be read: only their association is replayed. */
private withoutHistory = 0;
constructor(
private readonly history: SubagentHistory,
private readonly publish: Publish,
private readonly models?: SubagentModels,
/**
* Whether the replay maps a model to the session's model list as it goes.
* A load lacks the list until the CLI started, so it maps only in
* {@link currentStates}, after the load response.
*/
private readonly mapsDuringReplay = true,
/** The working directory of the root session, which a child inherits unless it reports its own. */
private readonly rootCwd?: string,
) {
this.transcript = new ReplayTranscript(history);
}
/**
* Registers the launches, the launch results, and the SendMessage calls in
* `messages`, the transcript of the session `ownerSessionId`.
*/
scan(messages: readonly SessionMessage[], ownerSessionId: string): void {
this.transcript.scan(messages, ownerSessionId);
}
/** The child that the tool use `toolUseId` launched, when the replay shows it. */
childOfLaunch(toolUseId: string): ReplayChild | undefined {
return this.children.get(toolUseId);
return this.transcript.children.get(toolUseId);
}
/**
@@ -211,11 +259,11 @@ export class RfdSubagentReplay {
*/
private effectiveCwd(sessionId: string): string | undefined {
const seen = new Set<string>();
let child = this.byAgentId.get(sessionId);
let child = this.transcript.byAgentId.get(sessionId);
while (child && !seen.has(child.sessionId)) {
if (child.cwd) return child.cwd;
seen.add(child.sessionId);
child = this.byAgentId.get(child.parentSessionId);
child = this.transcript.byAgentId.get(child.parentSessionId);
}
return this.rootCwd;
}
@@ -299,7 +347,9 @@ export class RfdSubagentReplay {
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
return false;
}
const sendMessage = this.sendMessages.find((call) => call.toolUseId === update.toolCallId);
const sendMessage = this.transcript.sendMessages.find(
(call) => call.toolUseId === update.toolCallId,
);
if (sendMessage) {
if (sendMessage.recipientSessionId === undefined) {
sendMessage.shownAsToolCall = true;
@@ -317,7 +367,7 @@ export class RfdSubagentReplay {
}
return true;
}
const handback = owner ? this.handbacks.get(update.toolCallId) : undefined;
const handback = owner ? this.transcript.handbacks.get(update.toolCallId) : undefined;
if (owner && handback && !handback.failed) {
if (!handback.sent) {
handback.sent = true;
@@ -333,7 +383,7 @@ export class RfdSubagentReplay {
}
return true;
}
const child = this.children.get(update.toolCallId);
const child = this.transcript.children.get(update.toolCallId);
if (!child) return false;
if (
update.status === "completed" &&
@@ -352,29 +402,29 @@ export class RfdSubagentReplay {
* its task id is the agent id.
*/
taskNotification(taskId: string, status: string): boolean {
const child = this.byAgentId.get(taskId);
const child = this.transcript.byAgentId.get(taskId);
if (!child) return false;
child.notified = nativeSubagentState(status) ?? child.notified;
return true;
}
/**
* Sends the end of every announced child (see `endState`), and a notice when
* Sends the end of every announced child (see `replayEndState`), and a notice when
* subagent history is missing.
*/
async finish(supportsNotices: boolean, rootSessionId: string): Promise<void> {
for (const child of [...this.children.values()].reverse()) {
for (const child of [...this.transcript.children.values()].reverse()) {
if (!child.announced) continue;
await this.publish({
sessionId: child.parentSessionId,
update: {
sessionUpdate: "subagent_update",
sessionId: child.sessionId,
state: endState(child),
state: replayEndState(child),
},
});
}
const missing = this.omitted.size + this.withoutHistory;
const missing = this.transcript.omitted.size + this.withoutHistory;
if (missing > 0 && supportsNotices) {
await this.publish({ sessionId: rootSessionId, update: missingHistoryNotice(missing) });
}
@@ -399,12 +449,14 @@ export class RfdSubagentReplay {
live: (sessionId: string) => WorkState["state"] | undefined,
liveConfiguration?: (sessionId: string) => SessionConfigOption[] | undefined,
): AcpSessionNotification[] {
return [...this.children.values()]
return [...this.transcript.children.values()]
.filter((child) => child.announced)
.flatMap((child): AcpSessionNotification[] => {
const liveState = live(child.sessionId);
const state: WorkState =
liveState === undefined || liveState === "idle" ? endState(child) : { state: liveState };
liveState === undefined || liveState === "idle"
? replayEndState(child)
: { state: liveState };
// The session's model list may be known only now, after the load response.
const entry = responseModelEntry(child.responseModel, this.models);
const replayed = child.model ? JSON.stringify([childModelOption(child.model)]) : undefined;
@@ -435,30 +487,6 @@ export class RfdSubagentReplay {
});
}
private registerLaunch(
toolUseId: string,
input: Record<string, unknown>,
parentSessionId: string,
): void {
if (this.children.has(toolUseId)) return;
const record = this.history.agents.get(toolUseId);
if (!record) {
this.omitted.add(toolUseId);
return;
}
const child: ReplayChild = {
launchId: toolUseId,
sessionId: record.agentId,
record,
parentSessionId,
input,
announced: false,
promptSent: false,
};
this.children.set(toolUseId, child);
this.byAgentId.set(record.agentId, child);
}
/** The prompt of the launch, with the launch's id, to the parent and the child, as live. */
private async sendPrompt(child: ReplayChild, text: string): Promise<void> {
child.promptSent = true;
@@ -489,7 +517,7 @@ export class RfdSubagentReplay {
text: string,
): Promise<void> {
const call = deliveredCall(
this.sendMessages,
this.transcript.sendMessages,
[child.sessionId, child.record.name, child.input.name],
text,
);
@@ -553,7 +581,7 @@ export function replaySubagentTerminalState(
* `cancelled` for a child the user stopped, else the result of its foreground
* launch, else `idle` with no stop reason.
*/
function endState(child: ReplayChild): WorkState {
export function replayEndState(child: ReplayChild): WorkState {
const end = child.notified ?? (child.record.stoppedByUser ? "cancelled" : child.launchEnd);
return end ? rfdEndState(end, undefined) : { state: "idle" };
}
@@ -586,7 +614,7 @@ function inputOf(block: Record<string, unknown>): Record<string, unknown> {
}
/** The text of user message content: a string, or its text blocks. */
function textOf(content: unknown): string {
export function textOf(content: unknown): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
return content
@@ -598,7 +626,7 @@ function textOf(content: unknown): string {
}
/** Whether `dir` is an existing directory. */
function isDirectory(dir: string): boolean {
export function isDirectory(dir: string): boolean {
try {
return statSync(dir).isDirectory();
} catch {

View File

@@ -3,7 +3,7 @@ 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 "../v1/acp-agent.js";
import { discoverCustomAgents, BUILTIN_AGENT_NAMES } from "../session-config-options.js";
// `discoverCustomAgents` distinguishes user/plugin-configured agents from
// Claude Code's built-in subagents by name (BUILTIN_AGENT_NAMES), since the

View File

@@ -15,8 +15,8 @@ import {
} from "../diff.js";
import { AcpToolCallRenderer } from "../tool-calls/renderer.js";
import { buildClaudePermissionPresentation } from "../permissions/presentation.js";
import { toolInfoFromToolUse } from "../tools.js";
import { WriteReporter } from "../tool-calls/reporters/file-edit.js";
import { toolInfoFromToolUse } from "./tool-render.js";
const tempDirectories: string[] = [];

View File

@@ -1,74 +1,22 @@
import { describe, expect, it } from "vitest";
import packageJson from "../../package.json" with { type: "json" };
describe("module runtime exports", () => {
/**
* The public runtime exports of lib.ts must change only deliberately; consumers import these names.
* The public runtime exports of lib.ts, the package's only entry point, must change only deliberately; consumers import these names.
*/
it("lib.ts exports the expected names", async () => {
expect(Object.keys(await import("../lib.js")).sort()).toEqual([
"AUTH_STATUS_UPDATE_METHOD",
"ClaudeAcpAgent",
"Pushable",
"SettingsManager",
"isLocalCommandMetadata",
"nodeToWebReadable",
"nodeToWebWritable",
"planEntries",
"runAcp",
"streamEventToAcpNotifications",
"stripLocalCommandMetadata",
"toAcpNotifications",
"toDisplayPath",
"toolInfoFromToolUse",
"toolUpdateFromToolResult",
"unreachable",
"serveAcp",
]);
});
/**
* Moving code out of acp-agent.ts must keep every name re-exported, since consumers deep-import
* dist/acp-agent.js; change this list only deliberately.
*/
it("acp-agent.ts exports the expected names", async () => {
expect(Object.keys(await import("../acp-agent.js")).sort()).toEqual([
"AGENT_CONFIG_ID",
"AUTONOMOUS_RESULT_ORIGINS",
"BUILTIN_AGENT_NAMES",
"ClaudeAcpAgent",
"DEFAULT_AGENT_ID",
"EFFORT_CONFIG_ID",
"FAST_MODE_CONFIG_ID",
"FAST_MODE_OFF",
"FAST_MODE_ON",
"MODEL_CONFIG_ID",
"MODE_CONFIG_ID",
"applyAvailableModelsAllowlist",
"buildConfigOptions",
"clientSupportsBooleanConfigOptions",
"clientSupportsRecommendedConfigValue",
"computeSessionFingerprint",
"createFastModeConfigOption",
"discoverCustomAgents",
"fastModeStateEnabled",
"getAvailableSlashCommands",
"isAbortedResult",
"isLocalCommandMetadata",
"isLoginRequiredText",
"isReplayHiddenMetaMessage",
"isSyntheticLoginMessage",
"matchResumedModel",
"messageIdForGrouping",
"normalizeFastModeDisabledReason",
"promptToClaude",
"resolveFastModeEnabled",
"resolveModelPreference",
"runAcp",
"runPromptWithCancellation",
"settingsEffortForModel",
"streamEventToAcpNotifications",
"stripLocalCommandMetadata",
"toAcpNotifications",
"v1AgentApp",
]);
it("package.json exposes lib.ts as the only module entry point", () => {
expect(Object.keys(packageJson.exports)).toEqual([".", "./package.json"]);
expect(packageJson.exports["."]).toEqual({
types: "./dist/lib.d.ts",
import: "./dist/lib.js",
});
});
});

View File

@@ -1,7 +1,10 @@
import { describe, expect, it } from "vitest";
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
import { buildConfigOptions, clientSupportsRecommendedConfigValue } from "../v1/acp-agent.js";
import {
buildConfigOptions,
clientSupportsRecommendedConfigValue,
} from "../session-config-options.js";
import { AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY } from "../air-extension.js";
const MODES = {

63
src/tests/tool-render.ts Normal file
View File

@@ -0,0 +1,63 @@
/**
* Test shorthands for the shared tool call renderer: one tool use or tool
* result, rendered for a client with the given capabilities. Production code
* renders through {@link AcpToolCallRenderer} directly.
*/
import type {
ToolCallContent,
ToolCallLocation,
ToolKind,
} from "@agentclientprotocol/sdk/unstable";
import { stripLocalCommandMetadata } from "../message-filters.js";
import { ClientCapabilities } from "../tool-calls/client-capabilities.js";
import type { ToolResultBlock } from "../tool-calls/content.js";
import { AcpToolCallRenderer, type RenderedResult } from "../tool-calls/renderer.js";
/**
* The title, kind, content, and locations of a tool use, for a client with the
* given terminal and patch capabilities. The {@link AcpToolCallRenderer} builds
* them from the facts of the tool reporter.
*/
export function toolInfoFromToolUse(
toolUse: any,
supportsTerminalOutput: boolean = false,
cwd?: string,
supportsDiffPatch: boolean = false,
): {
title: string;
kind: ToolKind;
content: ToolCallContent[];
locations?: ToolCallLocation[];
} {
const renderer = new AcpToolCallRenderer(
new ClientCapabilities(supportsTerminalOutput, false, supportsDiffPatch),
);
return renderer.toolInfo({ id: toolUse?.id, name: toolUse?.name, input: toolUse?.input }, cwd);
}
/**
* The result fields of a tool result, for a client with the given terminal
* capabilities. The {@link AcpToolCallRenderer} builds them from the facts of
* the tool reporter.
*/
export function toolUpdateFromToolResult(
toolResult: ToolResultBlock,
toolUse: any | undefined,
supportsTerminalOutput: boolean = false,
toolUseResult?: unknown,
preferTerminalOutputDelta: boolean = false,
): RenderedResult {
const renderer = new AcpToolCallRenderer(
new ClientCapabilities(supportsTerminalOutput, preferTerminalOutputDelta),
);
return renderer.resultFields(
{ id: toolUse?.id, name: toolUse?.name ?? "", input: toolUse?.input },
toolResult as Parameters<AcpToolCallRenderer["resultFields"]>[1],
toolUseResult,
);
}
/** Whether `content` is only Claude Code's local-command metadata, which the agent drops. */
export function isLocalCommandMetadata(content: unknown): boolean {
return stripLocalCommandMetadata(content) === null;
}

View File

@@ -26,12 +26,10 @@ import { AcpClient } from "../acp-client.js";
import { toAcpNotifications, ToolUseCache } from "../sdk-to-acp.js";
import { Logger } from "../logger.js";
import {
toolUpdateFromToolResult,
createPostToolUseHook,
createTaskHook,
clearHookCallbacks,
registerHookCallback,
toolInfoFromToolUse,
planEntries,
applyTaskCreate,
applyTaskList,
@@ -43,6 +41,7 @@ import {
} from "../tools.js";
import { ClientCapabilities as ToolCallCapabilities } from "../tool-calls/client-capabilities.js";
import { AcpToolCallRenderer } from "../tool-calls/renderer.js";
import { toolInfoFromToolUse, toolUpdateFromToolResult } from "./tool-render.js";
/** An AIR client: it gets the ACP tool call contract (`docs/air-extensions.md`). */
const AIR_CLIENT: ClientCapabilities = {

View File

@@ -2,9 +2,9 @@ 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 { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { makeMockQuery } from "./helpers.js";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import { makeMockQuery } from "../helpers.js";
const { querySpy } = vi.hoisted(() => ({
querySpy: vi.fn(),
@@ -85,7 +85,7 @@ describe("ClaudeAcpAgent settings", () => {
const { getCapturedOptions } = mockQuery();
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -129,7 +129,7 @@ describe("ClaudeAcpAgent settings", () => {
applyFlagSettings,
}),
);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent = new ClaudeAcpAgent(createMockClient());
if (recommended) {
(agent as any).clientCapabilities = {
@@ -180,7 +180,7 @@ describe("ClaudeAcpAgent settings", () => {
applyFlagSettings,
}),
);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent = new ClaudeAcpAgent(createMockClient());
(agent as any).clientCapabilities = {
_meta: { jetbrains: { air: { version: 1, capabilities: ["recommendedValue"] } } },
@@ -227,7 +227,7 @@ describe("ClaudeAcpAgent settings", () => {
applyFlagSettings,
}),
);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent = new ClaudeAcpAgent(createMockClient());
(agent as any).clientCapabilities = {
_meta: { jetbrains: { air: { version: 1, capabilities: ["recommendedValue"] } } },
@@ -271,7 +271,7 @@ describe("ClaudeAcpAgent settings", () => {
const { getCapturedOptions } = mockQuery();
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -296,7 +296,7 @@ describe("ClaudeAcpAgent settings", () => {
const { getCapturedOptions } = mockQuery();
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -319,7 +319,7 @@ describe("ClaudeAcpAgent settings", () => {
const { getCapturedOptions } = mockQuery();
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
(agent as any).logger = { log: () => {}, error: () => {} };
@@ -363,7 +363,7 @@ describe("ClaudeAcpAgent settings", () => {
const { getCapturedOptions } = mockQuery();
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
(agent as any).logger = { log: () => {}, error: () => {} };
@@ -388,7 +388,7 @@ describe("ClaudeAcpAgent settings", () => {
const { getCapturedOptions } = mockQuery();
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -414,7 +414,7 @@ describe("ClaudeAcpAgent settings", () => {
mockQuery();
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
(agent as any).logger = { log: () => {}, error: () => {} };
@@ -448,7 +448,7 @@ describe("ClaudeAcpAgent settings", () => {
);
const { getCapturedOptions, setModelSpy } = mockQuery(models);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
(agent as any).logger = { log: () => {}, error: () => {} };
return { agent, projectDir, getCapturedOptions, setModelSpy };
@@ -647,7 +647,7 @@ describe("ClaudeAcpAgent settings", () => {
const { getCapturedOptions } = mockQuery();
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -674,7 +674,7 @@ describe("ClaudeAcpAgent settings", () => {
const { setModelSpy } = mockQuery();
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -729,7 +729,7 @@ describe("ClaudeAcpAgent settings", () => {
},
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -756,7 +756,7 @@ describe("ClaudeAcpAgent settings", () => {
},
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -808,7 +808,7 @@ describe("ClaudeAcpAgent settings", () => {
{ value: "claude-haiku-4-5", displayName: "Claude Haiku", description: "Fast" },
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -845,7 +845,7 @@ describe("ClaudeAcpAgent settings", () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -892,7 +892,7 @@ describe("ClaudeAcpAgent settings", () => {
},
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -957,7 +957,7 @@ describe("ClaudeAcpAgent settings", () => {
{ value: "haiku", displayName: "Haiku", description: "Fast" },
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -997,7 +997,7 @@ describe("ClaudeAcpAgent settings", () => {
{ value: "haiku", displayName: "Haiku", description: "Fast" },
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -1029,7 +1029,7 @@ describe("ClaudeAcpAgent settings", () => {
{ value: "haiku", displayName: "Haiku", description: "Fast" },
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -1051,7 +1051,7 @@ describe("ClaudeAcpAgent settings", () => {
{ value: "haiku", displayName: "Haiku", description: "Fast" },
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -1085,7 +1085,7 @@ describe("ClaudeAcpAgent settings", () => {
{ value: "haiku", displayName: "Haiku", description: "Fast" },
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -1136,7 +1136,7 @@ describe("ClaudeAcpAgent settings", () => {
},
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -1197,7 +1197,7 @@ describe("ClaudeAcpAgent settings", () => {
},
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -1249,7 +1249,7 @@ describe("ClaudeAcpAgent settings", () => {
},
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -1304,7 +1304,7 @@ describe("ClaudeAcpAgent settings", () => {
},
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -1355,7 +1355,7 @@ describe("ClaudeAcpAgent settings", () => {
{ value: "sonnet", displayName: "Sonnet", description: "Claude Sonnet 4.6" },
]);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -1410,7 +1410,7 @@ describe("ClaudeAcpAgent settings", () => {
}) as any;
});
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -1451,7 +1451,7 @@ describe("ClaudeAcpAgent settings", () => {
});
try {
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
@@ -1502,7 +1502,7 @@ describe("ClaudeAcpAgent settings", () => {
}) as any;
});
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({

View File

@@ -22,48 +22,44 @@ import {
WriteTextFileRequest,
WriteTextFileResponse,
} from "@agentclientprotocol/sdk/unstable";
import type { AcpSessionNotification } from "../acp-subagents.js";
import { NativeSubagentRuntime } from "../native-subagents.js";
import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
import {
createPostToolUseHook,
markdownEscape,
toolInfoFromToolUse,
toDisplayPath,
toolUpdateFromToolResult,
} from "../tools.js";
import { toolUpdateFromDiffToolResponse } from "../diff.js";
import { ToolCallFieldTracker } from "../tool-calls/field-tracker.js";
import type { AcpSessionNotification } from "../../acp-subagents.js";
import { NativeSubagentRuntime } from "../../native-subagents.js";
import { nodeToWebWritable, nodeToWebReadable } from "../../utils.js";
import { createPostToolUseHook, markdownEscape, toDisplayPath } from "../../tools.js";
import { toolUpdateFromDiffToolResponse } from "../../diff.js";
import { ToolCallFieldTracker } from "../../tool-calls/field-tracker.js";
import {
ClaudeAcpAgent,
computeSessionFingerprint,
buildConfigOptions,
createFastModeConfigOption,
discoverCustomAgents,
runPromptWithCancellation,
type SteerRequest,
} from "../v1/acp-agent.js";
} from "../../v1/acp-agent.js";
import { buildConfigOptions, discoverCustomAgents } from "../../session-config-options.js";
import { createFastModeConfigOption } from "../../session-fast-mode.js";
import {
toAcpNotifications,
streamEventToAcpNotifications,
messageIdForGrouping,
type StreamedToolInputCache,
} from "../sdk-to-acp.js";
import { promptToClaude } from "../prompt-to-claude.js";
} from "../../sdk-to-acp.js";
import { promptToClaude } from "../../prompt-to-claude.js";
import {
isLocalCommandMetadata,
isReplayHiddenMetaMessage,
isSyntheticLoginMessage,
stripLocalCommandMetadata,
} from "../message-filters.js";
import type { AcpClient } from "../acp-client.js";
import type { SDKMessageFilter } from "../new-session-meta.js";
import { SessionTitles } from "../session-titles.js";
import { SessionBackgroundWork } from "../v1/background-work.js";
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "../usage-markdown.js";
import { Pushable } from "../utils.js";
import { heldRequest, initializeClient, makeMockQuery } from "./helpers.js";
import { acpProtocolRouter } from "../serve.js";
} from "../../message-filters.js";
import type { AcpClient } from "../../acp-client.js";
import type { SDKMessageFilter } from "../../new-session-meta.js";
import { SessionTitles } from "../../session-titles.js";
import { SessionBackgroundWork } from "../../v1/background-work.js";
import {
formatUsageResponse,
isUsageCommandText,
parseUsageResponse,
} from "../../usage-markdown.js";
import { Pushable } from "../../utils.js";
import { heldRequest, initializeClient, makeMockQuery } from "../helpers.js";
import { acpProtocolRouter } from "../../serve.js";
import * as agentSdk from "@anthropic-ai/claude-agent-sdk";
import {
deleteSession,
@@ -95,7 +91,7 @@ import {
goalUpdateFromPrompt,
parseGoalRequest,
toGoalSnapshot,
} from "../v1/goal-extension.js";
} from "../../v1/goal-extension.js";
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
const actual = await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>();
@@ -120,7 +116,12 @@ import type {
BetaWebFetchToolResultBlockParam,
BetaCodeExecutionToolResultBlockParam,
} from "@anthropic-ai/sdk/resources/beta.mjs";
import { validateRecorded } from "./acp-scenarios/schema.js";
import { validateRecorded } from "../acp-scenarios/schema.js";
import {
isLocalCommandMetadata,
toolInfoFromToolUse,
toolUpdateFromToolResult,
} from "../tool-render.js";
/** The capabilities of an AIR client. Only AIR gets the AIR extensions of
* `docs/air-extensions.md`. */
@@ -3605,7 +3606,7 @@ describe("usage-limit failure replay", () => {
it("recognizes a sanitized raw transcript through the real SDK parser", async () => {
const raw = await readFile(
new URL("./fixtures/usage-limit-session.jsonl", import.meta.url),
new URL("../fixtures/usage-limit-session.jsonl", import.meta.url),
"utf8",
);
const entries = raw

View File

@@ -5,11 +5,11 @@
* - Recordings are JSON Lines: one outbound message on each line, with sorted
* keys. `normalize` in `harness.ts` replaces only exact run-specific values:
* the generated ids, the paths, and the argv and version of this process.
* - AIR golden files: `acp-scenarios/__snapshots__/air/<scenario>.jsonl`.
* Run `npx vitest run src/tests/acp-scenarios.test.ts -u` to update them.
* - AIR golden files: `../acp-scenarios/__snapshots__/air/<scenario>.jsonl`.
* Run `npx vitest run src/tests/v1/acp-scenarios.test.ts -u` to update them.
* - Schema: every message is valid against the ACP schema of the SDK.
* - Plain and Zed: the same information as origin/main
* (`acp-scenarios/origin-main/<profile>/<scenario>.jsonl`). `compare.ts`
* (`../acp-scenarios/origin-main/<profile>/<scenario>.jsonl`). `compare.ts`
* allows only the documented differences. These profiles have no golden
* files of their own. `origin-main/zed/` holds a scenario only when its
* recording differs from the plain client.
@@ -17,9 +17,9 @@
* - AIR: the AIR extensions of `docs/air-extensions.md`, each fact once.
*
* To record the origin/main baseline again, copy `src/tests/acp-scenarios/`
* and this file into a checkout of origin/main, and run this file there with
* and this file to the same paths in a checkout of origin/main, and run this file there with
* `ACP_SCENARIO_BASELINE_DIR=<dir>`. Then copy `<dir>/plain` and `<dir>/zed`
* to `acp-scenarios/origin-main/`.
* to `src/tests/acp-scenarios/origin-main/`.
*/
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import * as fs from "node:fs";
@@ -41,20 +41,20 @@ import {
streamMessage,
toolCall,
toolResult,
} from "./acp-scenarios/harness.js";
import { SCENARIOS } from "./acp-scenarios/scenarios.js";
} from "../acp-scenarios/harness.js";
import { SCENARIOS } from "../acp-scenarios/scenarios.js";
import {
AIR_ONLY_CLAUDE_CODE_KEYS,
AIR_ONLY_META_KEYS,
canonical,
compareWithBaseline,
} from "./acp-scenarios/compare.js";
import { EXTENSION_SESSION_UPDATES, validateRecorded } from "./acp-scenarios/schema.js";
import { LEGACY_AIR_CUSTOM_ANSWER_KEY } from "../air-extension.js";
} from "../acp-scenarios/compare.js";
import { EXTENSION_SESSION_UPDATES, validateRecorded } from "../acp-scenarios/schema.js";
import { LEGACY_AIR_CUSTOM_ANSWER_KEY } from "../../air-extension.js";
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
const actual = await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>();
const harness = await import("./acp-scenarios/harness.js");
const harness = await import("../acp-scenarios/harness.js");
return {
...actual,
query: harness.mockedQuery,
@@ -66,7 +66,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
// learns each id that `randomUUID` returns.
vi.mock("node:crypto", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:crypto")>();
const harness = await import("./acp-scenarios/harness.js");
const harness = await import("../acp-scenarios/harness.js");
return {
...actual,
randomUUID: (...args: Parameters<typeof actual.randomUUID>) => {
@@ -97,7 +97,7 @@ beforeAll(async () => {
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "/usr/bin/false");
configDir = fs.mkdtempSync(path.join(os.tmpdir(), "acp-scenario-config-"));
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
Agent = ClaudeAcpAgent;
for (const profile of profiles) {
for (const scenario of SCENARIOS) {
@@ -189,7 +189,7 @@ function metaObjects(value: unknown, at = "$"): { at: string; meta: Record<strin
/** The origin/main recording of a scenario. Zed has a file only where it differs from the plain client. */
function readBaseline(profile: "plain" | "zed", scenario: string): string {
const file = (name: string) =>
path.join(here, "acp-scenarios", "origin-main", name, `${scenario}.jsonl`);
path.join(here, "..", "acp-scenarios", "origin-main", name, `${scenario}.jsonl`);
return fs.readFileSync(
profile === "zed" && !fs.existsSync(file("zed")) ? file("plain") : file(profile),
"utf8",
@@ -219,7 +219,7 @@ describe.skipIf(baselineDir)("ACP scenarios", () => {
const second = "cccccccc-0000-4000-8000-000000000003";
const scripted = "00000000-0000-4000-8000-00000000c001";
const version = JSON.parse(
fs.readFileSync(path.join(here, "..", "..", "package.json"), "utf8"),
fs.readFileSync(path.join(here, "..", "..", "..", "package.json"), "utf8"),
).version;
const argv = process.argv.slice(1);
@@ -322,7 +322,7 @@ describe.skipIf(baselineDir)("ACP scenarios", () => {
describe("air golden files", () => {
it.each(SCENARIOS.map((scenario) => scenario.name))("%s", async (scenario) => {
await expect(toJsonLines(run("air", scenario).normalized)).toMatchFileSnapshot(
path.join(here, "acp-scenarios", "__snapshots__", "air", `${scenario}.jsonl`),
path.join(here, "..", "acp-scenarios", "__snapshots__", "air", `${scenario}.jsonl`),
);
});
});

View File

@@ -3,8 +3,8 @@ import * as os from "node:os";
import * as path from "node:path";
import { SessionNotification } from "@agentclientprotocol/sdk/unstable";
import type { Options } from "@anthropic-ai/claude-agent-sdk";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
let capturedOptions: Options | undefined;
@@ -12,7 +12,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
"@anthropic-ai/claude-agent-sdk",
);
const { makeMockQuery } = await import("./helpers.js");
const { makeMockQuery } = await import("../helpers.js");
return {
...actual,
query: ({ options }: { options: Options }) => {
@@ -32,8 +32,8 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
},
};
});
vi.mock("../tools.js", async () => ({
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
vi.mock("../../tools.js", async () => ({
...(await vi.importActual<typeof import("../../tools.js")>("../../tools.js")),
registerHookCallback: vi.fn(),
}));
@@ -47,7 +47,7 @@ describe("additionalRoots", () => {
capturedOptions = undefined;
tempDirs.length = 0;
vi.resetModules();
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
agent = new ClaudeAcpAgent({
sessionUpdate: async (_notification: SessionNotification) => {},
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),

View File

@@ -1,12 +1,17 @@
import type { Mock } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import type { Logger } from "../logger.js";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AuthStatus } from "../auth-status.js";
import { fromAccountInfo, fromCliStatus, mergeAuthStatus, sameIdentity } from "../auth-status.js";
import { DEFAULT_CONTEXT_USAGE, makeMockQuery } from "./helpers.js";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import type { Logger } from "../../logger.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AuthStatus } from "../../auth-status.js";
import {
fromAccountInfo,
fromCliStatus,
mergeAuthStatus,
sameIdentity,
} from "../../auth-status.js";
import { DEFAULT_CONTEXT_USAGE, makeMockQuery } from "../helpers.js";
import { randomUUID } from "node:crypto";
import * as fs from "node:fs/promises";
import * as path from "node:path";

View File

@@ -4,16 +4,16 @@ import os from "node:os";
import path from "node:path";
import { describe, expect, it, Mock, vi, afterEach, beforeEach } from "vitest";
import type { AccountInfo } from "@anthropic-ai/claude-agent-sdk";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import { AcpClient } from "../acp-client.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import { AcpClient } from "../../acp-client.js";
import {
billsClaudeSubscription,
CLAUDE_LOGIN_REQUIRED_MESSAGE,
CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_MESSAGE,
CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_REASON,
} from "../hide-claude-auth.js";
import { SessionFailureController } from "../session-failure-extension.js";
import { DEFAULT_CONTEXT_USAGE, makeMockQuery } from "./helpers.js";
} from "../../hide-claude-auth.js";
import { SessionFailureController } from "../../session-failure-extension.js";
import { DEFAULT_CONTEXT_USAGE, makeMockQuery } from "../helpers.js";
const mockQuery = vi.hoisted(() => vi.fn());
@@ -1713,7 +1713,7 @@ describe("authorization", () => {
describe("session failure reason field", () => {
it("carries reason only when the publisher passes one", async () => {
const { SessionFailureController, createSessionFailureState } =
await import("../session-failure-extension.js");
await import("../../session-failure-extension.js");
const capabilities = {
_meta: { jetbrains: { air: { version: 1, capabilities: ["sessionFailure"] } } },
} as any;

View File

@@ -11,11 +11,11 @@ import * as fsSync from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import type { SessionInfo } from "@agentclientprotocol/sdk/unstable";
import type { ActivityState } from "../v1/session-index/activity.js";
import { endedTaskId, searchBackgroundTasks } from "../v1/session-index/background-tasks.js";
import { encodeProjectPath } from "../v1/session-index/project-dirs.js";
import { SessionIndex } from "../v1/session-index/session-index.js";
import { SessionIndexService } from "../v1/session-index/service.js";
import type { ActivityState } from "../../v1/session-index/activity.js";
import { endedTaskId, searchBackgroundTasks } from "../../v1/session-index/background-tasks.js";
import { encodeProjectPath } from "../../v1/session-index/project-dirs.js";
import { SessionIndex } from "../../v1/session-index/session-index.js";
import { SessionIndexService } from "../../v1/session-index/service.js";
let configDir: string;
let workspace: string;

View File

@@ -6,12 +6,12 @@ import {
} from "@agentclientprotocol/sdk/unstable";
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
import { getSessionMessages, type Options } from "@anthropic-ai/claude-agent-sdk";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import type { AcpSessionNotification } from "../acp-subagents.js";
import { ALLOW_BYPASS } from "../permissions/modes.js";
import { NativeSubagentRuntime } from "../native-subagents.js";
import { startupFailure } from "./helpers.js";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import type { AcpSessionNotification } from "../../acp-subagents.js";
import { ALLOW_BYPASS } from "../../permissions/modes.js";
import { NativeSubagentRuntime } from "../../native-subagents.js";
import { startupFailure } from "../helpers.js";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
@@ -38,7 +38,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
"@anthropic-ai/claude-agent-sdk",
);
const { makeMockQuery, DEFAULT_CONTEXT_USAGE } = await import("./helpers.js");
const { makeMockQuery, DEFAULT_CONTEXT_USAGE } = await import("../helpers.js");
return {
...actual,
query: (args: { prompt: unknown; options: Options }) => {
@@ -77,8 +77,8 @@ function initializationResponse() {
};
}
vi.mock("../tools.js", async () => {
const actual = await vi.importActual<typeof import("../tools.js")>("../tools.js");
vi.mock("../../tools.js", async () => {
const actual = await vi.importActual<typeof import("../../tools.js")>("../../tools.js");
return {
...actual,
registerHookCallback: vi.fn(),
@@ -118,7 +118,7 @@ describe("createSession options merging", () => {
});
vi.resetModules();
const acpAgent = await import("../v1/acp-agent.js");
const acpAgent = await import("../../v1/acp-agent.js");
ClaudeAcpAgent = acpAgent.ClaudeAcpAgent;
agent = new ClaudeAcpAgent(createMockClient());

View File

@@ -1,10 +1,10 @@
import { randomUUID } from "node:crypto";
import { describe, expect, it, vi } from "vitest";
import type { SessionNotification } from "@agentclientprotocol/sdk/unstable";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import { Writable } from "node:stream";
import { EventLoopYielder, nodeToWebWritable, Pushable } from "../utils.js";
import { EventLoopYielder, nodeToWebWritable, Pushable } from "../../utils.js";
import {
mockSessionState,
successfulResultMessage,

View File

@@ -1,9 +1,12 @@
import { describe, it, expect, vi } from "vitest";
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
import type { FastModeDisabledReason, ModelInfo } from "@anthropic-ai/claude-agent-sdk";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import {
buildConfigOptions,
clientSupportsBooleanConfigOptions,
} from "../../session-config-options.js";
import {
createFastModeConfigOption,
fastModeStateEnabled,
normalizeFastModeDisabledReason,
@@ -11,9 +14,8 @@ import {
FAST_MODE_CONFIG_ID,
FAST_MODE_ON,
FAST_MODE_OFF,
ClaudeAcpAgent,
} from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
} from "../../session-fast-mode.js";
import type { AcpClient } from "../../acp-client.js";
const MODES = {
currentModeId: "default",

View File

@@ -3,13 +3,13 @@ import type { SessionNotification } from "@agentclientprotocol/sdk/unstable";
import { randomUUID } from "node:crypto";
import * as path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import {
AGENT_FILE_CHANGE_REPORT_CAPABILITY,
type AgentFileChangeReportResult,
} from "../file-change-audit.js";
import type { Pushable } from "../utils.js";
} from "../../file-change-audit.js";
import type { Pushable } from "../../utils.js";
type Scenario =
| "reported"
@@ -89,8 +89,8 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
};
});
vi.mock("../tools.js", async () => ({
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
vi.mock("../../tools.js", async () => ({
...(await vi.importActual<typeof import("../../tools.js")>("../../tools.js")),
registerHookCallback: vi.fn(),
}));
@@ -258,7 +258,7 @@ describe("native file-change report integration", () => {
readTextFile: async () => ({ content: "" }),
writeTextFile: async () => ({}),
} as unknown as AcpClient;
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(client, {
log: () => {},
error: () => {},

View File

@@ -20,11 +20,11 @@ import {
type Scenario,
system,
toolCall,
} from "./acp-scenarios/harness.js";
} from "../acp-scenarios/harness.js";
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
const actual = await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>();
const harness = await import("./acp-scenarios/harness.js");
const harness = await import("../acp-scenarios/harness.js");
return {
...actual,
query: harness.mockedQuery,
@@ -34,7 +34,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
vi.mock("node:crypto", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:crypto")>();
const harness = await import("./acp-scenarios/harness.js");
const harness = await import("../acp-scenarios/harness.js");
return {
...actual,
randomUUID: (...args: Parameters<typeof actual.randomUUID>) => {
@@ -170,7 +170,7 @@ beforeAll(async () => {
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "/usr/bin/false");
configDir = fs.mkdtempSync(path.join(os.tmpdir(), "acp-scenario-config-"));
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
for (const profile of runProfiles) {
resetIds();
const run = await unlessHung(runScenario(ClaudeAcpAgent, profile, scenario));

View File

@@ -1,8 +1,8 @@
import { describe, expect, it, vi } from "vitest";
import { randomUUID } from "node:crypto";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { Pushable } from "../utils.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import { Pushable } from "../../utils.js";
import {
mockSessionState,
successfulResultMessage,

View File

@@ -4,9 +4,9 @@ import { createHash, randomUUID } from "node:crypto";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as os from "node:os";
import { messageIdForGrouping } from "../sdk-to-acp.js";
import { forkSession } from "../fork-session.js";
import { encodeProjectPath } from "../v1/session-index/project-dirs.js";
import { messageIdForGrouping } from "../../sdk-to-acp.js";
import { forkSession } from "../../fork-session.js";
import { encodeProjectPath } from "../../v1/session-index/project-dirs.js";
// Use the real SDK against temporary transcripts, without starting Claude Code.
let root: string;

View File

@@ -9,9 +9,9 @@ import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Options, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
import type { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { initializeClient } from "./helpers.js";
import type { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import { initializeClient } from "../helpers.js";
import { successfulResultMessage } from "./session-doubles.js";
const execFileSpy = vi.hoisted(() => vi.fn());
@@ -37,7 +37,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
"@anthropic-ai/claude-agent-sdk",
);
const { makeMockQuery } = await import("./helpers.js");
const { makeMockQuery } = await import("../helpers.js");
return {
...actual,
query: ({ prompt, options }: { prompt: AsyncIterable<SDKUserMessage>; options: Options }) => {
@@ -52,8 +52,8 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
};
});
vi.mock("../tools.js", async () => ({
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
vi.mock("../../tools.js", async () => ({
...(await vi.importActual<typeof import("../../tools.js")>("../../tools.js")),
registerHookCallback: vi.fn(),
}));
@@ -228,7 +228,7 @@ function scriptClaudeCode({
async function connect(clientCapabilities: Record<string, unknown> = {}) {
vi.resetModules();
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const updates: Array<{ update: Record<string, any> }> = [];
const client = {
sessionUpdate: async (notification: { update: Record<string, any> }) => {

View File

@@ -1,8 +1,8 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { clearHookCallbacks, createPostToolUseHook, hasHookCallback } from "../tools.js";
import { Pushable } from "../utils.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import { clearHookCallbacks, createPostToolUseHook, hasHookCallback } from "../../tools.js";
import { Pushable } from "../../utils.js";
import {
mockSessionState,
successfulResultMessage,

View File

@@ -9,8 +9,8 @@ import type {
SessionNotification,
} from "@agentclientprotocol/sdk/unstable";
import { describe, expect, it } from "vitest";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
type JsonObject = Record<string, unknown>;

View File

@@ -9,8 +9,8 @@ import type {
SessionNotification,
} from "@agentclientprotocol/sdk/unstable";
import { describe, expect, it } from "vitest";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
type JsonObject = Record<string, unknown>;

View File

@@ -2,17 +2,17 @@ import { randomUUID } from "node:crypto";
import { describe, expect, it, vi } from "vitest";
import type { McpServerStatus } from "@anthropic-ai/claude-agent-sdk";
import type { SessionNotification } from "@agentclientprotocol/sdk/unstable";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import { stripLocalCommandMetadata } from "../message-filters.js";
import type { AcpClient } from "../acp-client.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import { stripLocalCommandMetadata } from "../../message-filters.js";
import type { AcpClient } from "../../acp-client.js";
import {
cleanMcpError,
formatMcpStatus,
MCP_AVAILABLE_COMMAND,
parseMcpCommand,
} from "../mcp-command.js";
import { Pushable } from "../utils.js";
import { initializeClient } from "./helpers.js";
} from "../../mcp-command.js";
import { Pushable } from "../../utils.js";
import { initializeClient } from "../helpers.js";
import {
mockSessionState,
successfulResultMessage,

View File

@@ -1,9 +1,9 @@
import { describe, it, expect } from "vitest";
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { promptSuggestionsMeta, promptSuggestionText } from "../prompt-suggestions.js";
import { Pushable } from "../utils.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import { promptSuggestionsMeta, promptSuggestionText } from "../../prompt-suggestions.js";
import { Pushable } from "../../utils.js";
import {
mockSessionState,
successfulResultMessage,

View File

@@ -3,10 +3,11 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ModelInfo, Options } from "@anthropic-ai/claude-agent-sdk";
import { ClaudeAcpAgent, FAST_MODE_CONFIG_ID } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY } from "../air-extension.js";
import { initializeClient, makeMockQuery } from "./helpers.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import { FAST_MODE_CONFIG_ID } from "../../session-fast-mode.js";
import type { AcpClient } from "../../acp-client.js";
import { AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY } from "../../air-extension.js";
import { initializeClient, makeMockQuery } from "../helpers.js";
import { successfulResultMessage, userEcho } from "./session-doubles.js";
const sdk = vi.hoisted(() => ({

View File

@@ -1,7 +1,7 @@
import { describe, expect, it, Mock, vi, afterEach, beforeEach } from "vitest";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import { AcpClient } from "../acp-client.js";
import { loggableBaseUrl } from "../providers.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import { AcpClient } from "../../acp-client.js";
import { loggableBaseUrl } from "../../providers.js";
const mockQuery = vi.hoisted(() =>
vi.fn(() => ({

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, vi } from "vitest";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import { mockSessionState } from "./session-doubles.js";
/** A stream that stays open, like the real one: a consumer that reaches its end

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from "vitest";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { Pushable } from "../utils.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import { Pushable } from "../../utils.js";
import { mockSessionState, successfulResultMessage, wrapQuery } from "./session-doubles.js";
const HUMAN = { kind: "human" };

View File

@@ -1,16 +1,16 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { SessionNotification } from "@agentclientprotocol/sdk/unstable";
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { makeMockQuery } from "./helpers.js";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import { makeMockQuery } from "../helpers.js";
const { registerHookCallbackSpy } = vi.hoisted(() => ({
registerHookCallbackSpy: vi.fn(),
}));
vi.mock("../tools.js", async () => {
const actual = await vi.importActual<typeof import("../tools.js")>("../tools.js");
vi.mock("../../tools.js", async () => {
const actual = await vi.importActual<typeof import("../../tools.js")>("../../tools.js");
return {
...actual,
registerHookCallback: registerHookCallbackSpy,
@@ -133,7 +133,7 @@ describe("session config options", () => {
registerHookCallbackSpy.mockClear();
vi.resetModules();
const acpAgent = await import("../v1/acp-agent.js");
const acpAgent = await import("../../v1/acp-agent.js");
ClaudeAcpAgent = acpAgent.ClaudeAcpAgent;
agent = new ClaudeAcpAgent(createMockClient());
@@ -1134,7 +1134,7 @@ describe("session config options", () => {
// model option lists it while it is current, so a client can send it back,
// and drops it once the session switches to a listed model.
it("lists an off-list current model until the session switches away", async () => {
const { buildModelConfigOption } = await import("../session-model.js");
const { buildModelConfigOption } = await import("../../session-model.js");
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
session.models = { ...session.models, currentModelId: "claude-offlist-9" };
session.modelInfos = [

View File

@@ -7,9 +7,9 @@
import { vi } from "vitest";
import { randomUUID } from "crypto";
import { SessionBackgroundWork } from "../v1/background-work.js";
import { PromptSuggestions } from "../prompt-suggestions.js";
import { SessionTitles } from "../session-titles.js";
import { SessionBackgroundWork } from "../../v1/background-work.js";
import { PromptSuggestions } from "../../prompt-suggestions.js";
import { SessionTitles } from "../../session-titles.js";
/** Stand-in agent for a `SessionTitles` whose test doesn't care about titles:
* swallows the publish and the log, and never matches the identity check. */

View File

@@ -11,8 +11,8 @@ import { tmpdir } from "node:os";
import path from "node:path";
import type { NewSessionRequest } from "@agentclientprotocol/sdk/unstable";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
function sse(type: string, data: object): string {
return `event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`;

View File

@@ -2,28 +2,28 @@ import { describe, expect, it } from "vitest";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { deriveActivity, selectCost, type ActivityState } from "../v1/session-index/activity.js";
import { LiveSessionRegistry, type LiveRecord } from "../v1/session-index/live-registry.js";
import { deriveActivity, selectCost, type ActivityState } from "../../v1/session-index/activity.js";
import { LiveSessionRegistry, type LiveRecord } from "../../v1/session-index/live-registry.js";
import {
encodeProjectPath,
isSessionId,
pathAndAncestors,
projectDirMatches,
} from "../v1/session-index/project-dirs.js";
} from "../../v1/session-index/project-dirs.js";
import {
decodeListCursor,
encodeListCursor,
parseListOptions,
parseRenameSessionRequest,
type ListScope,
} from "../v1/session-index/service.js";
} from "../../v1/session-index/service.js";
import {
effectiveTitle,
isArchivedTitle,
storedTitle,
titleRecords,
visibleTitle,
} from "../v1/session-index/archive-title.js";
} from "../../v1/session-index/archive-title.js";
import {
agentWorktreeOf,
continuedInSessionId,
@@ -33,11 +33,11 @@ import {
sdkTitles,
transcriptAgentName,
type TranscriptFacts,
} from "../v1/session-index/transcript-scan.js";
import { firstPrompt } from "../v1/session-index/first-prompt.js";
import { DirListings, statFiles } from "../v1/session-index/dir-listing.js";
import { SessionIndex, type IndexRow } from "../v1/session-index/session-index.js";
import { changeSignature, sessionInfoOf } from "../v1/session-index/session-info.js";
} from "../../v1/session-index/transcript-scan.js";
import { firstPrompt } from "../../v1/session-index/first-prompt.js";
import { DirListings, statFiles } from "../../v1/session-index/dir-listing.js";
import { SessionIndex, type IndexRow } from "../../v1/session-index/session-index.js";
import { changeSignature, sessionInfoOf } from "../../v1/session-index/session-info.js";
const SESSION = "11111111-1111-4111-8111-111111111111";
const lines = (...entries: object[]) => entries.map((entry) => JSON.stringify(entry)).join("\n");

View File

@@ -16,14 +16,14 @@ import {
listSessions,
} from "@anthropic-ai/claude-agent-sdk";
import { spawn } from "node:child_process";
import { ClaudeAcpAgent, v1AgentApp } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { ClaudeAcpAgent, v1AgentApp } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import {
client as acpClient,
type InitializeResponse,
type SessionInfo,
} from "@agentclientprotocol/sdk/unstable";
import { encodeProjectPath } from "../v1/session-index/project-dirs.js";
import { encodeProjectPath } from "../../v1/session-index/project-dirs.js";
import {
archiveInsteadOfDelete,
archiveTo,
@@ -31,25 +31,25 @@ import {
parseBatchGetSessionsRequest,
SessionIndexService,
writeCustomTitleSidecar,
} from "../v1/session-index/service.js";
import { scanTranscriptFile } from "../v1/session-index/transcript-scan.js";
import { repositoryWorktrees } from "../v1/session-index/worktrees.js";
} from "../../v1/session-index/service.js";
import { scanTranscriptFile } from "../../v1/session-index/transcript-scan.js";
import { repositoryWorktrees } from "../../v1/session-index/worktrees.js";
import {
ListSubscriptions,
MAX_SUBSCRIPTIONS,
parseListSubscribeRequest,
parseListUnsubscribeRequest,
type ListChanges,
} from "../v1/session-index/list-subscriptions.js";
import { SessionTitles } from "../session-titles.js";
import { initializeClient } from "./helpers.js";
} from "../../v1/session-index/list-subscriptions.js";
import { SessionTitles } from "../../session-titles.js";
import { initializeClient } from "../helpers.js";
import {
mockSessionState,
successfulResultMessage,
userEcho,
wrapQuery,
} from "./session-doubles.js";
import { Pushable } from "../utils.js";
import { Pushable } from "../../utils.js";
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof fs>();
@@ -1854,7 +1854,7 @@ describe("_session/list/subscribe", () => {
});
it("clears the failed turn of a session that runs here when a new turn starts", async () => {
const { noteSessionState } = await import("../v1/session-index/connection.js");
const { noteSessionState } = await import("../../v1/session-index/connection.js");
const session = { lastTurnFailed: true } as any;
noteSessionState(session, "idle", "running");
expect(session.lastTurnFailed).toBe(false);

View File

@@ -2,9 +2,9 @@ import { describe, it, expect } from "vitest";
import { randomUUID } from "crypto";
import { RequestError, SessionNotification } from "@agentclientprotocol/sdk/unstable";
import { query, getSessionMessages } from "@anthropic-ai/claude-agent-sdk";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import { AcpClient } from "../acp-client.js";
import { Pushable } from "../utils.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import { AcpClient } from "../../acp-client.js";
import { Pushable } from "../../utils.js";
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
function createMockClient(

View File

@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, it } from "vitest";
import type { SessionNotification } from "@agentclientprotocol/sdk/unstable";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { makeMockQuery } from "./helpers.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import { makeMockQuery } from "../helpers.js";
const SESSION_ID = "test-session-id";

View File

@@ -2,10 +2,10 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import { appendTitleContext, sanitizeTitle } from "../session-titles.js";
import { Pushable } from "../utils.js";
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import { appendTitleContext, sanitizeTitle } from "../../session-titles.js";
import { Pushable } from "../../utils.js";
import { getSessionInfo } from "@anthropic-ai/claude-agent-sdk";
import {
mockSessionState,

View File

@@ -3,14 +3,14 @@ import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
import { AcpClient } from "../acp-client.js";
import { toAcpNotifications, ToolUseCache } from "../sdk-to-acp.js";
import { ClientCapabilities as ToolCallCapabilities } from "../tool-calls/client-capabilities.js";
import { backgroundedBashToolCall, withRunningTerminal } from "../tool-calls/background.js";
import { ChangedMetaFilter } from "../v1/tool-calls/changed-meta-filter.js";
import { ToolCallFieldTracker } from "../tool-calls/field-tracker.js";
import { AcpToolCallRenderer, mcpToolName } from "../tool-calls/renderer.js";
import { changedTaskPlanEntries, createTaskHook, type TaskState } from "../tools.js";
import { AcpClient } from "../../acp-client.js";
import { toAcpNotifications, ToolUseCache } from "../../sdk-to-acp.js";
import { ClientCapabilities as ToolCallCapabilities } from "../../tool-calls/client-capabilities.js";
import { backgroundedBashToolCall, withRunningTerminal } from "../../tool-calls/background.js";
import { ChangedMetaFilter } from "../../v1/tool-calls/changed-meta-filter.js";
import { ToolCallFieldTracker } from "../../tool-calls/field-tracker.js";
import { AcpToolCallRenderer, mcpToolName } from "../../tool-calls/renderer.js";
import { changedTaskPlanEntries, createTaskHook, type TaskState } from "../../tools.js";
const logger = { log: () => {}, error: () => {} };

View File

@@ -5,10 +5,10 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { SessionNotification } from "@agentclientprotocol/sdk/unstable";
import type { ClaudeAcpAgent } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import type { TurnEvents } from "../v1/turn-events.js";
import { DEFAULT_CONTEXT_USAGE } from "./helpers.js";
import type { ClaudeAcpAgent } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import type { TurnEvents } from "../../v1/turn-events.js";
import { DEFAULT_CONTEXT_USAGE } from "../helpers.js";
import { randomUUID } from "node:crypto";
const mockQuery = vi.hoisted(() => vi.fn());
@@ -27,8 +27,8 @@ vi.mock("node:child_process", async () => ({
(args[args.length - 1] as (error: Error) => void)(new Error("no CLI in tests")),
}));
vi.mock("../tools.js", async () => ({
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
vi.mock("../../tools.js", async () => ({
...(await vi.importActual<typeof import("../../tools.js")>("../../tools.js")),
registerHookCallback: vi.fn(),
}));
@@ -199,7 +199,7 @@ describe("turn events", () => {
beforeEach(async () => {
vi.resetModules();
onSessionUpdate = () => {};
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
agent = new ClaudeAcpAgent(
{
sessionUpdate: async (notification: SessionNotification) => onSessionUpdate(notification),

View File

@@ -4,8 +4,8 @@ import * as os from "node:os";
import * as path from "node:path";
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
import type { ModelInfo, Query } from "@anthropic-ai/claude-agent-sdk";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import {
createUltracodeConfigOption,
modelSupportsUltracode,
@@ -16,8 +16,8 @@ import {
ULTRACODE_ON,
ultracodeSupported,
ultracodeWorkflowsEnabled,
} from "../session-ultracode.js";
import { makeMockQuery } from "./helpers.js";
} from "../../session-ultracode.js";
import { makeMockQuery } from "../helpers.js";
const { querySpy } = vi.hoisted(() => ({ querySpy: vi.fn() }));
@@ -169,7 +169,7 @@ describe("ultracode session config option", () => {
...(opts.getSettings ? { getSettings: opts.getSettings } : {}),
}),
);
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent({
sessionUpdate: async () => {},
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),

View File

@@ -11,29 +11,29 @@
* - Tool calls: the first report of each tool call has a title. v2 has no
* `tool_call`, so a report of a tool call that the client has not seen
* creates it, where v1 drops it.
* - Golden files: `acp-scenarios/__snapshots__/v2/<scenario>.jsonl` for each
* - Golden files: `../acp-scenarios/__snapshots__/v2/<scenario>.jsonl` for each
* passing scenario, every message of the run. Run
* `npx vitest run src/tests/acp-scenarios-v2.test.ts -u` to update them.
* `npx vitest run src/tests/v2/acp-scenarios-v2.test.ts -u` to update them.
*/
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import type { AgentHandle } from "../v2/agent.js";
import type { AgentHandle } from "../../v2/agent.js";
import {
resetIds,
runScenarioV2,
type ScenarioRunV2,
type WireRecorded,
} from "./acp-scenarios/harness.js";
import { SCENARIOS } from "./acp-scenarios/scenarios.js";
import { canonical } from "./acp-scenarios/compare.js";
import { validateV2Message } from "./acp-scenarios/schema.js";
} from "../acp-scenarios/harness.js";
import { SCENARIOS } from "../acp-scenarios/scenarios.js";
import { canonical } from "../acp-scenarios/compare.js";
import { validateV2Message } from "../acp-scenarios/schema.js";
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
const actual = await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>();
const harness = await import("./acp-scenarios/harness.js");
const harness = await import("../acp-scenarios/harness.js");
return {
...actual,
query: harness.mockedQuery,
@@ -46,7 +46,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
// learns each id that `randomUUID` returns.
vi.mock("node:crypto", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:crypto")>();
const harness = await import("./acp-scenarios/harness.js");
const harness = await import("../acp-scenarios/harness.js");
return {
...actual,
randomUUID: (...args: Parameters<typeof actual.randomUUID>) => {
@@ -84,7 +84,7 @@ beforeAll(async () => {
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "/usr/bin/false");
configDir = fs.mkdtempSync(path.join(os.tmpdir(), "acp-scenario-config-"));
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
const { acpProtocolRouter } = await import("../serve.js");
const { acpProtocolRouter } = await import("../../serve.js");
for (const scenario of SCENARIOS) {
resetIds();
const agents: { dispose(): Promise<void> }[] = [];
@@ -168,7 +168,7 @@ describe("ACP v2 scenarios", () => {
(scenario) => statusOf(scenario) === PASSES,
);
const golden = (scenario: string) =>
path.join(here, "acp-scenarios", "__snapshots__", "v2", `${scenario}.jsonl`);
path.join(here, "..", "acp-scenarios", "__snapshots__", "v2", `${scenario}.jsonl`);
it.each(passing)("%s", async (scenario) => {
await expect(toJsonLines(run(scenario).normalized)).toMatchFileSnapshot(golden(scenario));
});

View File

@@ -18,25 +18,25 @@ import {
type Options,
type SDKUserMessage,
} from "@anthropic-ai/claude-agent-sdk";
import type { AuthStatusUpdateNotification } from "../auth-status.js";
import { clearContextWindowCache } from "../context-window.js";
import { v2DiffContent } from "../diff.js";
import { type AcpProtocols, acpProtocolRouter, acpProtocolsFromEnv } from "../serve.js";
import { v1PermissionResponse, v2PermissionRequest } from "../v2/permissions.js";
import { v1PromptRequest } from "../v2/prompt.js";
import { V2Terminals } from "../v2/terminal.js";
import { V2ToolCallMeta } from "../v2/tool-call-meta.js";
import { v2RestartedToolCall } from "../v2/tool-call.js";
import { v2SessionUpdate } from "../v2/session-update.js";
import type { AgentHandle } from "../v2/agent.js";
import type { AuthStatusUpdateNotification } from "../../auth-status.js";
import { clearContextWindowCache } from "../../context-window.js";
import { v2DiffContent } from "../../diff.js";
import { type AcpProtocols, acpProtocolRouter, acpProtocolsFromEnv } from "../../serve.js";
import { v1PermissionResponse, v2PermissionRequest } from "../../v2/permissions.js";
import { v1PromptRequest } from "../../v2/prompt.js";
import { V2Terminals } from "../../v2/terminal.js";
import { V2ToolCallMeta } from "../../v2/tool-call-meta.js";
import { v2RestartedToolCall } from "../../v2/tool-call.js";
import { v2SessionUpdate } from "../../v2/session-update.js";
import type { AgentHandle } from "../../v2/agent.js";
import {
v1ForkSessionRequest,
v1NewSessionRequest,
v1SetSessionConfigOptionRequest,
v2ConfigOptions,
} from "../v2/session-requests.js";
import { startupFailure } from "./helpers.js";
import packageJson from "../../package.json" with { type: "json" };
} from "../../v2/session-requests.js";
import { startupFailure } from "../helpers.js";
import packageJson from "../../../package.json" with { type: "json" };
const execFileSpy = vi.hoisted(() => vi.fn());
@@ -118,7 +118,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
"@anthropic-ai/claude-agent-sdk",
);
const { makeMockQuery } = await import("./helpers.js");
const { makeMockQuery } = await import("../helpers.js");
return {
...actual,
query: ({ prompt, options }: { prompt: AsyncIterable<SDKUserMessage>; options: Options }) => {
@@ -161,8 +161,8 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
};
});
vi.mock("../tools.js", async () => ({
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
vi.mock("../../tools.js", async () => ({
...(await vi.importActual<typeof import("../../tools.js")>("../../tools.js")),
registerHookCallback: vi.fn(),
}));

View File

@@ -4,7 +4,7 @@ import * as os from "node:os";
import * as path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import * as v2 from "@agentclientprotocol/sdk/experimental/v2/unstable";
import { nodeToWebReadable, nodeToWebWritable } from "../utils.js";
import { nodeToWebReadable, nodeToWebWritable } from "../../utils.js";
/**
* The ACP v2 core (`src/v2/`) against the bundled Claude Code

View File

@@ -20,10 +20,10 @@ import {
type Options,
type SDKUserMessage,
} from "@anthropic-ai/claude-agent-sdk";
import { acpProtocolRouter } from "../serve.js";
import { SettingsManager } from "../settings.js";
import type { AgentHandle } from "../v2/agent.js";
import { StoppedWork } from "../v2/stopped-work.js";
import { acpProtocolRouter } from "../../serve.js";
import { SettingsManager } from "../../settings.js";
import type { AgentHandle } from "../../v2/agent.js";
import { StoppedWork } from "../../v2/stopped-work.js";
/** One Claude Code process: the prompts it reads and the messages it sends. */
type FakeClaudeCode = {
@@ -160,8 +160,8 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
};
});
vi.mock("../diff.js", async () => {
const actual = await vi.importActual<typeof import("../diff.js")>("../diff.js");
vi.mock("../../diff.js", async () => {
const actual = await vi.importActual<typeof import("../../diff.js")>("../../diff.js");
return {
...actual,
previewPatchContent: async (...args: Parameters<typeof actual.previewPatchContent>) => {
@@ -172,14 +172,16 @@ vi.mock("../diff.js", async () => {
});
// The subagents that a replayed transcript launched, as their metadata files name them.
vi.mock("../subagent-history.js", async () => ({
...(await vi.importActual<typeof import("../subagent-history.js")>("../subagent-history.js")),
vi.mock("../../subagent-history.js", async () => ({
...(await vi.importActual<typeof import("../../subagent-history.js")>(
"../../subagent-history.js",
)),
subagentHistory: vi.fn(async () => ({ agents: sdk.subagents })),
readSubagentMessages: vi.fn(async () => []),
}));
vi.mock("../tools.js", async () => ({
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
vi.mock("../../tools.js", async () => ({
...(await vi.importActual<typeof import("../../tools.js")>("../../tools.js")),
registerHookCallback: vi.fn(),
}));

View File

@@ -1,9 +1,4 @@
import type {
PlanEntry,
ToolCallContent,
ToolCallLocation,
ToolKind,
} from "@agentclientprotocol/sdk/unstable";
import type { PlanEntry } from "@agentclientprotocol/sdk/unstable";
import { HookCallback } from "@anthropic-ai/claude-agent-sdk";
import type {
TaskCreateInput,
@@ -12,56 +7,9 @@ import type {
TaskUpdateInput,
TaskUpdateOutput,
} from "@anthropic-ai/claude-agent-sdk/sdk-tools.js";
import { ClientCapabilities } from "./tool-calls/client-capabilities.js";
import type { ToolResultBlock } from "./tool-calls/content.js";
import { AcpToolCallRenderer, type RenderedResult } from "./tool-calls/renderer.js";
export { markdownEscape, toDisplayPath } from "./tool-calls/content.js";
/**
* The title, kind, content, and locations of a tool use, for a client with the
* given terminal and patch capabilities. The {@link AcpToolCallRenderer} builds
* them from the facts of the tool reporter.
*/
export function toolInfoFromToolUse(
toolUse: any,
supportsTerminalOutput: boolean = false,
cwd?: string,
supportsDiffPatch: boolean = false,
): {
title: string;
kind: ToolKind;
content: ToolCallContent[];
locations?: ToolCallLocation[];
} {
const renderer = new AcpToolCallRenderer(
new ClientCapabilities(supportsTerminalOutput, false, supportsDiffPatch),
);
return renderer.toolInfo({ id: toolUse?.id, name: toolUse?.name, input: toolUse?.input }, cwd);
}
/**
* The result fields of a tool result, for a client with the given terminal
* capabilities. The {@link AcpToolCallRenderer} builds them from the facts of
* the tool reporter.
*/
export function toolUpdateFromToolResult(
toolResult: ToolResultBlock,
toolUse: any | undefined,
supportsTerminalOutput: boolean = false,
toolUseResult?: unknown,
preferTerminalOutputDelta: boolean = false,
): RenderedResult {
const renderer = new AcpToolCallRenderer(
new ClientCapabilities(supportsTerminalOutput, preferTerminalOutputDelta),
);
return renderer.resultFields(
{ id: toolUse?.id, name: toolUse?.name ?? "", input: toolUse?.input },
toolResult as Parameters<AcpToolCallRenderer["resultFields"]>[1],
toolUseResult,
);
}
export type ClaudePlanEntry = {
content: string;
status: "pending" | "in_progress" | "completed";

View File

@@ -25,7 +25,6 @@ import {
LoadSessionResponse,
LogoutRequest,
methods,
ndJsonStream,
NewSessionRequest,
NewSessionResponse,
PromptRequest,
@@ -312,14 +311,7 @@ import {
isFinalToolCallStatus,
type ToolCallUpdate,
} from "../tool-calls/renderer.js";
import {
messageLoopYielder,
nodeToWebReadable,
nodeToWebWritable,
Pushable,
raceTimeoutAndAbort,
unreachable,
} from "../utils.js";
import { messageLoopYielder, Pushable, raceTimeoutAndAbort, unreachable } from "../utils.js";
import {
acceptedPlanToolResult,
ExitPlanCoordinator,
@@ -330,7 +322,6 @@ import { AGENT_CONFIG_ID, DEFAULT_AGENT_ID } from "../session-config-ids.js";
import { type PersistedTaskNotification, taskNotificationsOf } from "../task-notifications.js";
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "../usage-markdown.js";
export { DEFAULT_AGENT_ID } from "../session-config-ids.js";
import { parseMcpCommand, runMcpCommand } from "../mcp-command.js";
import { configuredSettings, resolveThinkingConfig } from "../session-env.js";
import {
@@ -408,33 +399,6 @@ import { STARTUP_FAILURE_RESULTS_ENV, startupFailureError } from "../startup-fai
import { recordStreamedDelta, unstreamedRemainder, type StreamedBlock } from "../streamed-text.js";
import { subagentHookOptions } from "../subagent-configuration.js";
export {
AGENT_CONFIG_ID,
BUILTIN_AGENT_NAMES,
buildConfigOptions,
clientSupportsBooleanConfigOptions,
clientSupportsRecommendedConfigValue,
type ConfigOptionPresentation,
discoverCustomAgents,
MODE_CONFIG_ID,
} from "../session-config-options.js";
export {
createFastModeConfigOption,
FAST_MODE_CONFIG_ID,
FAST_MODE_OFF,
FAST_MODE_ON,
fastModeStateEnabled,
type FastModeOptionState,
normalizeFastModeDisabledReason,
resolveFastModeEnabled,
} from "../session-fast-mode.js";
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "../session-effort.js";
export {
applyAvailableModelsAllowlist,
matchResumedModel,
MODEL_CONFIG_ID,
resolveModelPreference,
} from "../session-model.js";
import type { AcpClient } from "../acp-client.js";
import type { Logger } from "../logger.js";
import {
@@ -456,28 +420,6 @@ import {
} from "../sdk-to-acp.js";
import { getAvailableSlashCommands } from "../slash-commands.js";
// Moved to their own modules; kept as exports for consumers that deep-import dist/acp-agent.js
// (re-exported by `../acp-agent.ts`).
export { AUTONOMOUS_RESULT_ORIGINS, isAbortedResult, isLoginRequiredText } from "../result-end.js";
export type { AcpClient } from "../acp-client.js";
export type { Logger } from "../logger.js";
export {
isLocalCommandMetadata,
isReplayHiddenMetaMessage,
isSyntheticLoginMessage,
stripLocalCommandMetadata,
} from "../message-filters.js";
export type { NewSessionMeta, SDKMessageFilter } from "../new-session-meta.js";
export { promptToClaude } from "../prompt-to-claude.js";
export {
messageIdForGrouping,
streamEventToAcpNotifications,
toAcpNotifications,
type StreamedToolInputCache,
type ToolUseCache,
} from "../sdk-to-acp.js";
export { getAvailableSlashCommands } from "../slash-commands.js";
/** Claude CLI emits this synthetic result when an interrupted cycle ends on
* queued user input before producing any assistant content. It is a hand-off
* marker, not the outcome of the replacement prompt that may already be
@@ -1746,8 +1688,6 @@ export function computeSessionFingerprint(params: {
* cannot drift from the errors this agent throws. */
const AUTH_REQUIRED_CODE = RequestError.authRequired().code;
export type { ToolUpdateMeta } from "../tool-calls/renderer.js";
const SUBAGENT_TRANSCRIPT_CAPABILITY = "subagent-transcript";
function supportsSubagentTranscript(capabilities?: ClientCapabilities | null): boolean {
@@ -11319,15 +11259,6 @@ export function v1AgentApp(
return sessionIndex.onSessionIndexRequests(app, () => agent);
}
/** Serves ACP v1 on stdio. */
export function runAcp(logger?: Logger) {
const stream = ndJsonStream(nodeToWebWritable(process.stdout), nodeToWebReadable(process.stdin));
let agent!: ClaudeAcpAgent;
// `connect` runs the connect handlers before it returns, so `agent` is set.
const connection = v1AgentApp(logger, (created) => (agent = created)).connect(stream);
return { connection, agent };
}
/**
* What a `command_lifecycle` "started" frame says started the command: a
* ScheduleWakeup (dynamic `/loop`) wakeup, with the time it was set for in

View File

@@ -35,40 +35,36 @@
* parent, and a notice at the end says that subagent history is missing, as
* it does for a child whose transcript cannot be read.
*/
import * as fs from "node:fs";
import type * as v2 from "@agentclientprotocol/sdk/experimental/v2/unstable";
import type { SessionConfigOption } from "@agentclientprotocol/sdk/unstable";
import type { SessionMessage } from "@anthropic-ai/claude-agent-sdk";
import type { Logger } from "../logger.js";
import type { AcpSessionNotification, SubagentState } from "../acp-subagents.js";
import type { AcpSessionNotification } from "../acp-subagents.js";
import {
childModelOption,
HANDBACK_TOOL,
handbackMessage,
isNativeSubagentControlTool,
launchReply,
linkedSessionMessages,
nativeSubagentState,
responseModelEntry,
rfdEndState,
rfdLabels,
type ModelEntry,
type SubagentModels,
type WorkState,
} from "../native-subagents.js";
import {
readSubagentMessages,
subagentHistory,
subagentLaunchIds,
type SubagentHistory,
type SubagentRecord,
} from "../subagent-history.js";
import {
deliveredCall,
firstResponseModel,
isDelivery,
isDirectory,
missingHistoryNotice,
replaySubagentTerminalState,
ReplayTranscript,
replayEndState,
textOf,
type ReplayChild as SharedReplayChild,
} from "../subagent-replay.js";
import { taskNotificationsOf } from "../task-notifications.js";
import type { V2UpdateTranslator } from "./update-translator.js";
@@ -96,53 +92,11 @@ export interface LiveChildren {
const NO_LIVE_CHILDREN: LiveChildren = { tracks: () => false, configuration: () => undefined };
type ReplayChild = {
launchId: string;
sessionId: string;
record: SubagentRecord;
parentSessionId: string;
input: Record<string, unknown>;
/** How the launch's tool result ended a foreground child's run. */
launchEnd?: "completed" | "failed" | "cancelled";
/** The state of the child's last persisted task notification. */
notified?: SubagentState;
announced: boolean;
promptSent: boolean;
/** Whether it handed a report to its parent with a SubagentHandback call. */
handedBack?: boolean;
/** The `model` option entry that the replay sent. */
model?: ModelEntry;
/** The model id of its latest replayed response. */
responseModel?: string;
/** The `cwd` it was announced with, when not its parent's. */
cwd?: string;
output?: SessionOutput;
};
type SendMessageCall = {
toolUseId: string;
to: string;
message: string;
senderSessionId: string;
/** The child the replay delivered the message to. */
recipientSessionId?: string;
/** The replay reached the call before the delivery, so it stays a tool call. */
shownAsToolCall: boolean;
/** The sender's view of the message went out, at the call. */
outgoingSent: boolean;
};
/** A replayed child, with the output of its session once announced. */
type ReplayChild = SharedReplayChild & { output?: SessionOutput };
export class SubagentReplay {
/** The children, by the id of the call that launched each. */
private readonly children = new Map<string, ReplayChild>();
private readonly sendMessages: SendMessageCall[] = [];
/** The SubagentHandback calls, with their report and whether their result failed. */
private readonly handbacks = new Map<
string,
{ message: string; failed: boolean; sent: boolean }
>();
/** Launches without metadata, which the replay leaves out. */
private readonly omitted = new Set<string>();
private readonly transcript: ReplayTranscript<ReplayChild>;
/** Children whose transcript could not be read: only their association is replayed. */
private withoutHistory = 0;
private rootOutput: SessionOutput | undefined;
@@ -153,7 +107,9 @@ export class SubagentReplay {
/** The transcript of each child, by agent id. */
private readonly transcripts: Map<string, SessionMessage[]>,
private readonly live: LiveChildren,
) {}
) {
this.transcript = new ReplayTranscript(history);
}
/**
* Reads the subagent history that `messages`, the session's transcript,
@@ -183,7 +139,7 @@ export class SubagentReplay {
/** The hooks of the replay of the root session's history (`messages`). */
hooks(output: SessionOutput, messages: SessionMessage[]): ReplayHooks {
this.rootOutput = output;
this.scan(messages, output.sessionId);
this.transcript.scan(messages, output.sessionId);
return this.ownerHooks(output, undefined);
}
@@ -192,24 +148,24 @@ export class SubagentReplay {
* stream, and the notice when subagent history is missing.
*/
finish(): void {
const announced = [...this.children.values()].filter((child) => child.announced);
const announced = [...this.transcript.children.values()].filter((child) => child.announced);
for (const child of [...announced].reverse()) {
this.host.send({
sessionId: child.parentSessionId,
update: {
sessionUpdate: "subagent_update",
sessionId: child.sessionId,
state: endState(child) as v2.StateUpdate,
state: replayEndState(child) as v2.StateUpdate,
},
});
}
for (const child of announced) {
this.host.send({
sessionId: child.sessionId,
update: { sessionUpdate: "state_update", ...endState(child) } as v2.SessionUpdate,
update: { sessionUpdate: "state_update", ...replayEndState(child) } as v2.SessionUpdate,
});
}
const missing = this.omitted.size + this.withoutHistory;
const missing = this.transcript.omitted.size + this.withoutHistory;
if (missing > 0) {
this.rootOutput?.emitV1([
{ sessionId: this.host.sessionId, update: missingHistoryNotice(missing) },
@@ -227,7 +183,7 @@ export class SubagentReplay {
* the session's model list.
*/
afterResponse(): void {
for (const child of this.children.values()) {
for (const child of this.transcript.children.values()) {
if (!child.announced) continue;
if (!this.live.tracks(child.sessionId)) this.sendEndState(child);
this.sendConfiguration(child);
@@ -249,7 +205,7 @@ export class SubagentReplay {
}
private sendEndState(child: ReplayChild): void {
const state = endState(child);
const state = replayEndState(child);
this.host.send({
sessionId: child.parentSessionId,
update: {
@@ -280,63 +236,6 @@ export class SubagentReplay {
* Registers the launches, the launch results, the SendMessage calls and the
* SubagentHandback calls in `messages`, of the session `ownerSessionId`.
*/
private scan(messages: readonly SessionMessage[], ownerSessionId: string): void {
for (const message of messages) {
for (const block of contentBlocks(message)) {
if (block.type === "tool_use" && typeof block.id === "string") {
if (isNativeSubagentControlTool(block.name)) {
this.registerLaunch(block.id, inputOf(block), ownerSessionId);
} else if (block.name === HANDBACK_TOOL) {
const report = handbackMessage(block.input);
if (report)
this.handbacks.set(block.id, { message: report, failed: false, sent: false });
} else if (block.name === "SendMessage") {
const input = inputOf(block);
if (typeof input.to === "string" && typeof input.message === "string") {
this.sendMessages.push({
toolUseId: block.id,
to: input.to,
message: input.message,
senderSessionId: ownerSessionId,
shownAsToolCall: false,
outgoingSent: false,
});
}
}
} else if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
const handback = this.handbacks.get(block.tool_use_id);
if (handback && block.is_error === true) handback.failed = true;
const child = this.children.get(block.tool_use_id);
// A background launch answers at once; its result is not the end.
if (child && !child.record.background)
child.launchEnd = replaySubagentTerminalState(block);
}
}
}
}
private registerLaunch(
launchId: string,
input: Record<string, unknown>,
parentSessionId: string,
): void {
if (this.children.has(launchId)) return;
const record = this.history.agents.get(launchId);
if (!record) {
this.omitted.add(launchId);
return;
}
this.children.set(launchId, {
launchId,
sessionId: record.agentId,
record,
parentSessionId,
input,
announced: false,
promptSent: false,
});
}
/**
* A replayed update of a tool call of `output`, the session of `owner`
* when it is a child's. The client is not shown the launch of a replayed
@@ -354,7 +253,9 @@ export class SubagentReplay {
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
return [notification];
}
const sendMessage = this.sendMessages.find((call) => call.toolUseId === update.toolCallId);
const sendMessage = this.transcript.sendMessages.find(
(call) => call.toolUseId === update.toolCallId,
);
if (sendMessage) {
if (sendMessage.recipientSessionId === undefined) {
sendMessage.shownAsToolCall = true;
@@ -370,7 +271,7 @@ export class SubagentReplay {
);
return [outgoing!];
}
const handback = owner ? this.handbacks.get(update.toolCallId) : undefined;
const handback = owner ? this.transcript.handbacks.get(update.toolCallId) : undefined;
if (owner && handback && !handback.failed) {
if (handback.sent) return [];
handback.sent = true;
@@ -386,7 +287,7 @@ export class SubagentReplay {
this.outputOf(owner.parentSessionId)?.emitV1([incoming!]);
return [];
}
const child = this.children.get(update.toolCallId);
const child = this.transcript.children.get(update.toolCallId);
if (!child) return [notification];
if (
update.status === "completed" &&
@@ -412,7 +313,9 @@ export class SubagentReplay {
return;
}
for (const notification of taskNotificationsOf(inner?.content)) {
const child = [...this.children.values()].find((c) => c.sessionId === notification.task_id);
const child = [...this.transcript.children.values()].find(
(c) => c.sessionId === notification.task_id,
);
if (child) child.notified = nativeSubagentState(notification.status) ?? child.notified;
}
}
@@ -421,13 +324,13 @@ export class SubagentReplay {
private replayed(message: SessionMessage, parent: SessionOutput): void {
if (message.type !== "assistant") return;
for (const launchId of launchIdsOf(message)) {
const child = this.children.get(launchId);
const child = this.transcript.children.get(launchId);
if (!child || child.announced) continue;
const messages = this.transcripts.get(child.sessionId) ?? [];
if (messages.length === 0) this.withoutHistory++;
this.announce(child);
this.sendModel(child, firstResponseModel(messages));
this.scan(messages, child.sessionId);
this.transcript.scan(messages, child.sessionId);
child.output!.replay(messages, this.ownerHooks(child.output!, child));
if (!child.promptSent && typeof child.input.prompt === "string") {
this.sendPrompt(child, parent, child.input.prompt);
@@ -517,7 +420,7 @@ export class SubagentReplay {
*/
private sendDelivery(child: ReplayChild, message: SessionMessage, text: string): void {
const call = deliveredCall(
this.sendMessages,
this.transcript.sendMessages,
[child.sessionId, child.record.name, child.input.name],
text,
);
@@ -553,63 +456,18 @@ export class SubagentReplay {
private outputOf(sessionId: string): SessionOutput | undefined {
if (sessionId === this.host.sessionId) return this.rootOutput;
return [...this.children.values()].find((child) => child.sessionId === sessionId)?.output;
return [...this.transcript.children.values()].find((child) => child.sessionId === sessionId)
?.output;
}
private cwdOf(sessionId: string): string {
const child = [...this.children.values()].find((c) => c.sessionId === sessionId);
const child = [...this.transcript.children.values()].find((c) => c.sessionId === sessionId);
if (!child) return this.host.cwd;
return child.cwd ?? this.cwdOf(child.parentSessionId);
}
}
/**
* How a replayed child's work ended: the state of its last task
* notification, else a stop by the user, else its foreground launch's result.
*/
function endState(child: ReplayChild): WorkState {
const end =
child.notified ??
nativeSubagentState(child.record.stoppedByUser ? "cancelled" : child.launchEnd);
return end ? rfdEndState(end, undefined) : { state: "idle" };
}
function launchIdsOf(message: SessionMessage): string[] {
if (message.type !== "assistant") return [];
return subagentLaunchIds((message.message as { content?: unknown } | undefined)?.content);
}
function contentBlocks(message: SessionMessage): Record<string, unknown>[] {
const content = (message as { message?: { content?: unknown } }).message?.content;
return Array.isArray(content)
? content.filter(
(block): block is Record<string, unknown> => typeof block === "object" && block !== null,
)
: [];
}
function inputOf(block: Record<string, unknown>): Record<string, unknown> {
return typeof block.input === "object" && block.input !== null
? (block.input as Record<string, unknown>)
: {};
}
/** The text of user message content: a string, or its text blocks. */
function textOf(content: unknown): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
return content
.flatMap((block) =>
typeof block === "object" && block !== null && block.type === "text" ? [block.text] : [],
)
.filter((text): text is string => typeof text === "string")
.join("\n");
}
function isDirectory(dir: string): boolean {
try {
return fs.statSync(dir).isDirectory();
} catch {
return false;
}
}