Commit Graph

4 Commits

Author SHA1 Message Date
Ben Brandt
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.
2026-10-02 18:15:39 +02:00
Ben Brandt
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.
2026-10-02 17:51:47 +02:00
nikita-ashihmin
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.
2026-09-28 17:58:00 +04:00
nikita-ashihmin
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)
2026-09-15 12:39:39 +04:00