mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-10 11:21:06 +00:00
bc031f362cb48e92349c7c8a4742e92c266d1c0d
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cb285cd285 |
feat: serve ACP v2 with a direct v2 agent and remove the v1 bridge (#1560)
## Summary ACP v2 was served by a bridge (`src/v2/agent.ts`) that converted each v2 request to v1 and ran it through `ClaudeAcpAgent`. Every v2 session therefore ran the v1 core's turn machinery: held prompts, echo matching, trailing-idle debt and the hold for background subagents. v2 asks for none of it. This PR serves v2 with its own agent in `src/v2/`, which follows Claude Code's own events. A prompt is answered when Claude Code takes it in, and `state_update` reports foreground work. Background subagents and tasks are listed in `background` and do not hold the turn. - **New v2 agent (`src/v2/`).** `V2Agent` and `V2Session` cover: - the prompt lifecycle and cancellation - subagents as child sessions, following the subagents RFD, plus background lists - replay, restarts for provider and route changes, auth and providers - elicitation, config options, `/remote-control`, and the `--hide-claude-auth` guard - **Bridge removed.** `ClaudeAcpAgent` serves ACP v1 only: its `v2` option, foreground/background hooks and `this.v2` branches are gone. v1 behavior is unchanged, and no v1 test expectation or golden file changed. The control flow of `acp-agent.ts` is simpler and the file is about 450 lines shorter. - **Shared modules.** Protocol-neutral logic moved out of `acp-agent.ts` into modules both versions use, including: - `session-model.ts`, `session-effort.ts`, `session-config-options.ts` and `session-env.ts` - `providers.ts`, `auth-status-reporter.ts` and `context-window.ts` - `result-end.ts`: one set of rules for reading a result in both cores - `prompt-suggestions.ts` and `mcp-servers.ts` - **JetBrains AIR on v2.** An AIR v2 client gets the AIR extensions that ACP v2 does not cover: - `recommendedValue` - the permission presentation - question custom answers - prompt suggestions - `agentFileChangeReport`, sent at the end of the turn, before its idle ACP v2 itself covers subagent sessions, background subagents, async tasks, plan files, diff patches, compaction, custom instructions and session failures. See "ACP v2" in `docs/air-extensions.md`. - **Ported from main.** Main's v2 changes made on the bridge are ported to the new agent, and main's v2 tests run against it: - workflow cards and phases (#1550, #1552) - task tool calls (#1556) - plan removal on reset (#1554) - subagent checklist plan ids (#1558) - the Fable alias (#1548) ## Behavior changes for v2 clients Each one follows the v2 draft spec, where the bridge did not: - **Background subagents.** They no longer hold the prompt open. They are listed in `background`, and their work is reported in their child session's state. - **`requires_action`.** While the root waits on foreground subagents, it reports `requires_action` only when every one of them waits on the user. - **Resume of an active session.** Its work keeps running, and the new MCP servers replace the old ones (`setMcpServers`). The bridge tore the session down. - **Cancel.** The idle after a cancel waits for Claude Code's aborted result and carries its usage. A stopped child's open tool calls end before its idle. - **`/remote-control`.** It runs as its own turn. Sent while Claude works, it waits for that work to end. - **Accepted URL elicitation.** It keeps the work `requires_action` until Claude Code reports the elicitation complete. - **Exited Claude Code.** A session whose Claude Code process exited stays active and answers -32603 with what happened, until it is resumed. - **Provider change.** `providers/set`/`disable` restarts idle sessions before it answers. A busy session restarts at its next turn boundary, and its prompts are not held meanwhile, which the provider RFD allows. ## Workarounds - **`/remote-control` waits outside the prompt queue.** The adapter's own `/clear` and plan prompts, which continue a turn in a fresh context, use their own push queue (`ownPushes`). They must not wait behind a `/remote-control` that waits for that same turn to end. - **Restart on additional directories.** A resume with other additional directories restarts the query at its next turn boundary. The SDK cannot change the directories of a running query. ## Rollback `CLAUDE_AGENT_ACP_PROTOCOL=v1` negotiates every client down to v1. The bridge and its `CLAUDE_AGENT_ACP_V2_CORE` switch are removed. ## Not served on v2 yet - AIR's tool call keys (`commandTitle`, `subagent`, `skill`, `mcp`), the session index, and API retry warnings. - A `replayFrom` cursor other than `start`, which is rejected with -32602, and steering, which is v1 only. ## Packaging - `lib.js` exports are unchanged. - `acp-agent.js` exports no name fewer than before. It adds `AUTONOMOUS_RESULT_ORIGINS`, `getAvailableSlashCommands` and `isAbortedResult`, which the v2 agent uses, and the export pin test is updated. - Internal `dist/v2/*` modules changed. They are reachable only through the `"./*"` export and are not documented. ## Testing - `npm run format:check`, `lint`, `build` and `test:run` pass: 3935 passed, 33 skipped. - `npm run test:integration` against the real CLI passes: 3960 passed, 8 skipped. This includes `src/tests/v2-integration.test.ts`. - The v2 golden scenarios (`acp-scenarios-v2.test.ts`) match without being regenerated. - The binary builds and `scripts/smoke-binary.mjs` passes. - Two tests unchanged from main can time out under heavy load: the session-index file watcher test and v1's "lagged trailer" test. Both pass on rerun. ## Follow-up - Split `src/` into v1, v2 and shared modules. - Move the SDK-to-ACP mapping that `src/v2/` still imports from `acp-agent.ts` into a shared module. - Add a lint rule that keeps the boundary. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c3428de1bf |
fix: keep native Windows paths in v2 git patches (#1492)
ACP v2 now says each `git_patch` path, once unquoted, is the same native absolute path string as the matching `changes` entry (agentclientprotocol/agent-client-protocol#2650). The v2 form converted `C:\work\App.ts` to `C:/work/App.ts`, so on Windows the patch and `changes[].path` named different strings. The v2 form now keeps the native path, which Git quoting writes as `"C:\\work\\App.ts"`. Git's own form, which the AIR extension uses, still uses forward slashes and the `a/`/`b/` prefixes. |
||
|
|
ff01e06579 |
fix: update to ACP SDK 2.0.0 (#1386)
Updates `@agentclientprotocol/sdk` from 1.9.0 to 2.0.0. SDK 2.0.0 makes the default entrypoints stable-only and removes the legacy connection classes ([migration guide](https://github.com/agentclientprotocol/typescript-sdk/blob/main/MIGRATION_STABLE_UNSTABLE.md)). - Protocol imports move to `@agentclientprotocol/sdk/unstable` and `@agentclientprotocol/sdk/experimental/v2/unstable`. The agent uses unstable features (session forking, `systemPrompt`, turn usage, subagents, v2 draft extras), so this keeps the same protocol surface as 1.9.0. No wire changes. - The scenario schema validator loads `schema/v1/schema.unstable.json`, because the unversioned `schema/schema.json` export was removed. - `remote-control.test.ts` uses the repo's own `AcpClient` type instead of the removed `AgentSideConnection`. - Runtime code already used the `agent()` app API, so it needed no migration. Verified: `npm run build`, `npm run test:run` (3199 passed), `npm run check`, and a separate typecheck of `examples/`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
03d84f6663 |
feat: add the AIR sessionIndex extension for fast session lists (#1268)
## Summary
Adds the opt-in AIR `sessionIndex` extension. With it, an AIR client can
show the latest sessions of a project quickly, with their state, and can
rename, archive and unarchive them over ACP. The full contract is in
`docs/air-extensions.md` ("Session index").
The extension follows ACP RFDs. The transport differs: it uses
`_meta.jetbrains.air` request options and row fields, `_session/*`
methods, and AIR capabilities that are not advertised under ACP v2. The
`archived` list filter also differs (see below).
- **Session list extensions**
(agentclientprotocol/agent-client-protocol#2323):
- `limit`, ordered by last user activity (`lastPromptAt ?? updatedAt`),
and `includeWorktrees`.
- Row fields `lastPromptAt`, `model` and `forkedFrom`.
- `state`: `running`, `idle`, `requires_action`, and `error`, which the
RFD lacks.
- `lastTurnEndedAt`.
- `cost`: the SDK's `total_cost_usd` for a session that runs on this
connection only. No cost is read from transcripts.
- Live updates come from an AIR list subscription (below).
- **Session archive** (agentclientprotocol/agent-client-protocol#2161):
- `archived` is on every row, `session_info_update` is sent on a change,
and an unknown session is `-32002`.
- The `archived` list filter has three values: `"unarchived"` (the
default), `"archived"` (archived sessions only) and `"all"` (both, in
one order). #2161 has a boolean here. Its `false` is `"unarchived"`, its
`true` is `"all"`, and `"archived"` has no counterpart.
- **Capabilities**: an AIR client that declares `sessionIndex` gets
these in `_meta.jetbrains.air.capabilities`. The client declares only
`sessionIndex`.
- `sessionIndex`.
- `sessionArchive`: `_session/archive` and `_session/unarchive`. The RFD
capability is `archive` (#2161).
- `sessionRename`: `_session/rename`. The RFD has capability `setTitle`
and method `session/set_title`
(agentclientprotocol/agent-client-protocol#1987).
- `sessionListSubscribe`: `_session/list/subscribe`,
`_session/list/unsubscribe` and `_session/list/changes`. No RFD
counterpart.
### Clients without `sessionIndex`
Clients that don't declare `sessionIndex`, which means non-AIR clients
and released AIR builds, keep the current behaviour. They get the same
SDK `listSessions` path, page size, cursor and initialize capabilities,
with no watchers and no notifications. There are two deliberate
deviations, both strictly safer:
1. **`session/delete` from an AIR client without `sessionIndex`.**
Released AIR builds use delete as "Done". Instead of deleting the
transcript (data loss), the adapter archives the session in AIR's own
Claude format (below). Archived sessions are hidden from that client's
list, so "Done" looks the same and nothing is lost. An unknown session
keeps the SDK's error. Other clients get the SDK delete, as before.
2. **`session/close`** of a session that is not loaded here returns `{}`
instead of throwing.
### Sessions open in another process
`session/load`, `session/resume` and `session/prompt` work on a session
that another live Claude Code process has open, for every client, as the
Claude CLI does. Rename, archive, unarchive and delete also go ahead,
writing records or deleting transcripts as the CLI's `/rename` and
delete do. The other process keeps running.
### Archive format and rule
The archive state uses AIR's native Claude archive format
(`ClaudeStoreThreadBackend.appendTitleRecords`, ADR 0140), so AIR's
native path and the adapter read and write the same state.
- **Archive** appends `{"type":"custom-title","customTitle":"[archived]
<title>",…}` and `{"type":"agent-name","agentName":"[archived]
<title>",…}` to every transcript copy. **Unarchive** appends the same
pair without the prefix. Titles are cut to the CLI's 200 characters.
- **Archived is decided by the agent name first**, as in AIR's native
Claude provider. A session's name is its last top-level `agentName`,
else the custom title the SDK reports. The session is archived exactly
when that name starts with `[archived] `. The listed title comes from
the same source, shown without the prefix.
- Title records are metadata: `updatedAt` and the list order do not
move.
### Rename, archive and delete
- **Rename** writes the same title pair. An archived session keeps its
prefix. A session running here is renamed through its CLI's
`rename_session` control request, serialised with auto-titling. Other
sessions get the records plus the `custom-title.json` sidecar the CLI
writes. No generated title replaces a client title, but a title stored
later by someone else (a `/rename`) is still adopted.
- **Archiving stops the running session.** `_session/archive` of a
session loaded on this connection does four things:
1. It interrupts the running turn.
2. It retitles the session through its CLI. The adapter also appends the
matching `agent-name` record to the CLI's transcript, because the CLI
writes that record only later.
3. It closes the session as `session/close` does.
4. It archives the transcripts once more if the closed CLI left them
unarchived.
The client then gets `session_info_update` with `archived: true`.
Unarchive does not reopen the session. A session that is not loaded here
is archived by writing the title records.
- **Delete** for a `sessionIndex` client deletes every copy of the
session and its `<sessionId>/` directories. A session running here is
closed first.
### Implementation (`src/session-index/`)
- **Index**:
- Reads the project dirs with `readdir` + `stat`, and keeps a 2000-entry
metadata LRU keyed by (path, mtime, size).
- Cache misses are read in parallel through `getSessionInfo`, plus a
growing tail read (64 KB → 4 MB) for the last prompt, last message, turn
end and agent name.
- SDK semantics are kept: sidecar titles, `continued-in`, long-path
hashes, darwin case, and first-prompt extraction (ported, and
fuzz-compared with the SDK).
- The `cwd` scope follows the SDK `listSessions`. Worktrees expand to
the same relative subdirectory, and a session copied to several project
directories is listed from its newest copy.
- The keyset cursor is bound to `cwd`, `archived` and
`includeWorktrees`. Session ids match exactly.
- **State**:
- For sessions of this process, the SDK state.
- For other sessions, the Claude Code live registry
(`~/.claude/sessions/<pid>.json`, with pid + procStart liveness and
Claude Desktop's rules) or the transcript tail. The registry is read
only for the row state.
- `error` means the session is not running and its last turn ended with
an error, not a cancel.
Documented in `docs/air-extensions.md`, with an extension → RFD mapping
table and the remaining differences.
### Measured on a real `~/.claude` (233 transcripts, read-only)
| Case | New path | Old SDK path |
|---|---|---|
| cwd, limit 50, cold process | ~80 ms | 78–156 ms (all rows) |
| cwd, limit 50, warm | 1.3–4 ms | — |
## Session list subscription
Replaces polling the first page. It is only for a client that declared
`sessionIndex`; the agent advertises `sessionListSubscribe` with it,
never under ACP v2.
- **Subscribe**: `_session/list/subscribe { cwd }` returns `{
subscriptionId }`. `cwd` must be absolute, else `-32602`; other params
are ignored.
- The scope is `cwd` and the same subdirectory in every linked worktree,
in any archive state, without sidechains.
- **Changes**: `_session/list/changes { subscriptionId, sessions,
removed }`.
- `sessions` holds the full `session/list` row of each session that
appeared, or whose title, `lastPromptAt`, `state`, `lastTurnEndedAt`,
`cost`, `model`, `forkedFrom` or `archived` changed since the last row
sent.
- The first change of a session after subscribe is sent in full. After
that, a change of `updatedAt` alone sends nothing.
- `removed` lists sessions that were deleted or moved out of scope.
- Archive and unarchive are row changes.
- A session is sent at most once a second, with one notification per
subscription per pass.
- **Unsubscribe**: `_session/list/unsubscribe { subscriptionId }`
returns `{}` and is idempotent.
- A subscription lives until unsubscribe or until the connection closes.
- A connection has at most 128 subscriptions. The 129th is `-32602` with
`data.reason: "too_many_subscriptions"`.
- Subscriptions of the same `cwd` share one watch and are notified
independently.
- **Delivery** is best effort, with no resync and no sequence numbers. A
rescan every 10 s (and 1 s after a project directory watch opens) sends
whatever differs.
- **Implementation** (`list-subscriptions.ts`, `session-info.ts`):
- A non-recursive `fs.watch` on the scope's project dirs, the registry
and the projects root.
- A transcript event re-reads only that transcript, and a registry event
reads only that `<pid>.json`. Sessions running here push their SDK state
at once.
- Subscribe reads no session row. The rescan reads only transcripts
whose stat changed or whose read failed.
### Measured: one subscription on the 5,200-session project (Docker
`node:22`, 2 CPU, 2 GB, shared host)
300 s of churn: 20 sessions appending 10/s each (updatedAt-only),
registry status flips every 200 ms, and a session created every 2 s and
deleted 10 s later.
| | Old `list_changed` watcher | Subscription |
|---|---|---|
| Agent CPU p50 / max | 19% / — | 12.9% / 49% |
| Event loop p99 (p50 / max of 1 s windows) | — | 16 / 31 ms; worst
single delay 87 ms |
| Change latency p50 / p95 | 950 ms (hint) | create 291 / 570 ms, delete
279 / 506 ms; registry state 902 / 990 ms* |
| Notifications / min | — | 90 (896 rows, 0 rows for updatedAt-only
appends) |
| RSS | flat ~160 MB | 159 → 198 MB (heap flat ~50 MB) |
\* The registry flips the same 5 sessions every 200 ms, so each is held
to the once-a-second limit. Latency is measured from the first unsent
change.
These numbers were measured with an earlier subscribe that read every
row up front. Subscribe now reads no rows, so that one-off cost no
longer occurs. The lazy subscription has not been re-measured yet.
## Load test: 10,000 sessions (Docker, 2 CPU, 2 GB)
Calibrated data: 10k transcripts, 15 GB, 36k subagent files, and one
5,200-session project. Sizes match a real profile (p50 311K / p90 2.67M
/ p99 20.5M).
| Scenario (limit 50) | Before | After |
|---|---|---|
| Big project, first page cold / warm p50 | out of memory | 225 / 19 ms
|
| No cwd, cold / warm p50 | 1,722 / 178 ms | 209 / 45 ms |
| Page through 4,637 rows | 457 s | 11.4 s cold, 2.9 s warm |
| Delete p50 / 100 concurrent | 23.8 ms / 4.4 s | 7.8 ms / 0.8 s |
| 300 s of churn (60k changes) | out of memory after 3 s | list p50 54
ms, 0 stalls >50 ms, RSS flat ~160 MB |
## Streaming: keep the event loop responsive under a backlog
Under a backlog of buffered SDK messages, the adapter could freeze its
event loop for seconds. Every `query.next()` on a non-empty SDK queue
resolves in a microtask, so `runConsumer` forwarded the whole backlog
without reaching a macrotask boundary. Incoming JSON-RPC, including
`session/cancel`, waited until the backlog drained.
1. **Consumer yields to the event loop.** A shared `EventLoopYielder`
(`src/utils.ts`) gives the event loop a turn via `setImmediate` once the
current iteration has run for 8 ms. Order, content and cancel semantics
are unchanged.
2. **No per-message copy in `nodeToWebWritable`** for byte streams.
Object-mode streams keep the copy.
3. **Diff memo.** The approval preview and the PostToolUse patch no
longer diff the same texts twice. Timed-out diffs are not remembered.
4. **Prefix guards** in `parseMcpCommand` and `sanitizeTitle`. Both give
identical output.
5. **Replay yields between history messages**, with the same budget.
With 10 sessions × 1 MB, the max event-loop delay went from 4205 ms to
1626 ms, and ping p95 from 2793 ms to 262 ms (interleaved A/B, Docker
`node:22`, 2 CPU). Single-stream throughput is unchanged. Remaining
stalls are inside the Claude SDK and GC.
## Test plan
- [x] Rebased on `origin/main`. `npm run check`, `npm run build` and
`npm run test:run` pass.
- [x] Regression tests for the unchanged non-`sessionIndex` paths.
- [x] Regression tests for every review finding.
- [x] Reviews: Codex (gpt-6-astra) and a Claude review subagent, run as
independent passes against `origin/main` until a round found nothing of
normal or higher severity.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
b9a6ec00a6 |
refactor: serve permission requests on the experimental ACP v2 surface (#1234)
First part of step 5 of opt-in ACP v2 support (after #1231, #1232): the v2 surface now sends the agent's permission requests. Nothing changes on ACP v1: no v1 golden file changes. ## What differs in v2 A v1 request asks about a tool call, and clients show the tool call's title as the prompt. A v2 request gives the prompt a required `title` and an optional `description` of its own, apart from the tool call. It asks about a `subject`, which can be a tool call or a command. ## Change - **Prompt text.** The agent already computes the prompt's heading and explanation, but only AIR gets them, in `_meta.jetbrains.air.permission`. The agent's request type, `AcpPermissionRequest`, now carries them as `title` and `description` for a v2 client. It's the same rule #1232 uses for diffs: build what v1 can't express only for a v2 client. So v1 requests are byte-identical, and the v1 scenario golden files, which record what the agent hands `requestPermission`, don't change. The v2 surface fails on a request without a title, so a missed site fails loudly. - **Subject.** Always `{ type: "tool_call", toolCall }`, where `toolCall` is the same patch a `tool_call_update` carries (`v2ToolCallPatch`). The `command` subject would suit Bash once display terminals exist, since its `terminalId` refers to one. - **Approval preview.** The exact patch before an Edit or Write runs is now built in v2 form for a v2 client, and the request shows it. This fixed a real gap: the agent pins the preview as content the client holds, but only AIR's request shape included it. Without the fix, a v2 client saw no diff at all, because the hook's identical patch was then dropped as unchanged. - **Answer.** v2's outcomes map back unchanged. An outcome v1 doesn't know, which the draft says is no approval, becomes `cancelled`, and the agent reads that as an aborted tool use. - **`requires_action`** while a request is open was already in place since #1226. ## Scenario suite All 12 scenarios that stopped at a permission request now pass: Bash, Edit (approved and rejected), Write, NotebookEdit, WebFetch, MCP, network, ExitPlanMode (approved and rejected), and two subagent scenarios. Each recording shows `requires_action`, the request, then `running`. Now only replay and the two plan scenarios remain. ## Verification - **End to end:** a new test drives the agent's real `canUseTool` through a v2 client. Allow, reject, and an unknown outcome become allow, deny, and an aborted tool use. - **Unit tests:** the prompt's title is separate from the tool call's (ExitPlanMode: "Ready to code?" vs "Approve Plan"), the preview shows in the request, the v2 preview fallback works, and the title guard rejects a missing title. - **Mutations:** each rule fails a test when removed. Sending `title`/`description` to every client fails 37 tests, including v1 golden files. - Build, lint and all 2380 tests pass. |
||
|
|
c1c8c0377d |
refactor: send structured diffs on the experimental ACP v2 surface (#1232)
Second part of step 4 of opt-in ACP v2 support (after #1231): the v2 surface now sends file changes as v2 diffs. Nothing changes on ACP v1: no v1 golden file changes. ## Why the agent builds them A v2 diff names each change's operation (`add`, `modify`, …) and can carry a `git_patch`. A v1 diff is a path with `oldText`/`newText`, and the agent often fills those with a snippet: an Edit's `old_string`/`new_string`, or one display hunk. So a v1 diff can't be translated. It says neither the operation (`oldText: null` can mean a new file, or a hunk with no old lines) nor where the lines are. So the agent builds v2 diffs itself, at the sites that build v1 diffs and know the change. The v2 surface creates the agent with `{ v2: true }`, which becomes `ToolCallClientCapabilities.v2`. A v1 client can't set it. This follows how the renderer already picks a form by capability, as it does for AIR's `diffPatch`. ## What a v2 client gets - **Live Edit and Write tool calls show no diff,** as a Write already does for AIR. The input holds a snippet, or doesn't say whether the file exists. - **The PostToolUse hook sends the exact patch** built from the written file. That's the existing AIR `diffPatch` path, which now produces either form. When it can't be exact, it falls back to a patch from the toolResponse: - a created file: `add` with its whole text; - an update: `modify` with the display hunks, which are fine for v2's renderable patch; - an update without hunks (previous content too large): the change alone; - an Edit with an empty `old_string`: no diff, since the response looks the same for a created file and a filled empty one. - **`git_patch` headers use absolute paths** without git's `a/` and `b/` prefixes, as the draft's example does: `diff --git /p/a.ts /p/a.ts`. ## Plumbing - The exported `toAcpNotifications` and `streamEventToAcpNotifications` take the agent's tool call capabilities (`toolCallCapabilities`) next to the ACP ones, which can't carry the v2 flag. External callers are unchanged. - The v2 diff travels through the agent as v1-typed content, with one documented cast in `v2DiffContent`. Nothing in the agent reads inside a diff item. - The v2 surface passes v2 diffs through and rejects a v1 diff, so a missed site fails loudly. That's how the scenario suite found the notification functions on the first run. - The approval preview stays AIR-only until v2 serves permission requests (step 5). ## Scenario suite `write-existing` passes, with an exact patch (golden file added). `write-new`, `edit-with-permission` and `edit-rejected` now stop at their permission request (step 5). No scenario stops at a diff any more. ## Verification - Unit tests cover the absolute-path headers, the live tool calls, the exact patch, and each fallback. - Each new rule fails a test when removed: git-style paths in a v2 patch, the v1 snippet for a v2 Edit, the AIR path for a v2 hook result, the notification functions ignoring the agent capabilities, and the missing guard for an Edit that created or filled a file. - Build, lint and all 2362 tests pass. |
||
|
|
875d75f2a4 |
feat: AIR tool call contract, exact git patches, plan file, and fixes for every client (#1153)
## Summary One PR with separate commits (the earlier stack #1168 and #1169 is folded back in). ### Exact git patches - `src/diff.ts` builds exact git patches for Edit and Write: real line numbers, file modes, CRLF, the final-newline marker, a size limit (including a bound for `replace_all`), binary detection, and an asynchronous diff with a time budget. The approval preview mirrors the input normalisation of the bundled CLI. - No creation patch over an existing file or for an Edit of an existing empty file, and no patch for a Write that changed nothing. ### For every client Clients that do not declare `_meta.jetbrains.air` (Zed, plain ACP clients) get the same information as on `main`. - A `tool_call_update` omits top-level fields that did not change since the last report of the same tool call (ACP merge semantics). The `_meta` of each report stays complete. - Bug fixes: a failed approval preview no longer blanks the approval diff; a permission denial replaces a pinned approval patch; the compaction summary is not repeated when the streamed text is identical; streamed subagent text is not sent twice; async task fields are recorded only after a successful send; late updates of a finished native subagent are dropped; the updates of a fresh Claude context after a clear-context restart reach the ACP session. ### For AIR only - One fact in one field: input in `rawInput`, the file text of an edit only in the diff (`diffPatch`), output once, no display copies of the input (`rawInputRendering`), `_meta.claudeCode` reduced to `toolName`, `parentToolUseId` and the `toolResponse` markers, per-key `_meta` filtering. - Plan file mode (`planFile`): ExitPlanMode carries only `rawInput.planFilePath`; AIR follows the file that Claude writes. - AIR-only keys move under `_meta.jetbrains.air.*` and are no longer sent to other clients. ### Structure and docs - `src/tool-calls/`: one reporter per tool, one `ClientCapabilities`, one `AcpToolCallRenderer`, the field tracker and the changed-meta filter. - `docs/air-extensions.md` replaces the per-extension AIR docs. ## Tests - `npm run build`, `npm run check`, `npm run test:run`: 1995 passed. - Scenario tests for plain, Zed, and AIR clients: the SDK mock honours the adapter's session id, every update must reach its ACP session, every message is validated against the ACP JSON schema with all formats checked, plain and Zed are compared with a recorded `main` baseline, AIR keeps compact golden recordings. |
||
|
|
735950a83e |
perf: supply AIR diff counts from Claude structured patches (#1122)
Claude supplies structured patches for Edit and Write, but ACP diff blocks previously carried only the reconstructed texts. AIR had to compare those texts again to display added and removed counts. Count patch operations in the existing conversion loop and publish `_meta.jetbrains.air.diffStats` on each diff block. This uses the same contract as https://github.com/agentclientprotocol/codex-acp/pull/501. Each block has its own counts. No extra traversal, full file comparison, or navigation calculation is needed. Omit statistics when patches are absent or inconsistent, so clients retain their existing fallback. Exclude EOF markers from the displayed texts. Document the extension and cover per-block counts, Edit and Write hooks, EOF changes, and fallback behavior. Validation: - `npm run build` - `npm run check` - `env -u ANTHROPIC_BASE_URL npm run test:run` (the local endpoint override affects unrelated provider tests) |