Nanocoder as an ACP agent - driving it from Zed #73
LottieOxford
announced in
Articles
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
The headline of v1.28.0 lists "ACP for Zed" alongside a tool-surface consolidation and a session-resume fix. The first item deserves its own write-up, because what actually shipped is a different agent: same Nanocoder, same tool definitions, same provider plumbing, but a headless core speaking JSON-RPC instead of an Ink terminal UI. This post walks through what changes when the editor becomes the UI, what gets rendered there, how the protocol mapping works, and how ACP differs from the WebSocket-based VS Code integration that has been around since the extension shipped.
Built by the Nano Collective - a community collective building AI tooling not for profit, but for the community.
The shortest possible description
There is a new flag:
When launched with
--acp, Nanocoder does not draw anything. It reads JSON-RPC from stdin and writes JSON-RPC to stdout. The editor spawns the process, owns the lifecycle, and renders everything - conversation text, tool cards, permission prompts,ask_useroptions, session modes, model selectors - using its own UI. The Ink terminal UI is replaced wholesale by the editor's agent panel.That is the whole shape. The interesting part is what got built to make it work.
The module:
source/acp/The ACP support lives in a new top-level module rather than being threaded through the existing UI code, which keeps the conversation loop and the Ink renderer independent of the JSON-RPC layer. As of v1.28.0 the module contains:
acp-server.ts- the@agentclientprotocol/sdkserver wiring, lifecycle, and stdio transport.acp-agent.ts- the top-levelAgentimplementation that the SDK calls back into.acp-session.ts- session lifecycle: creation, restoration, the in-memory history map keyed bysessionId.acp-conversation.ts- the conversation loop, streaming, tool-call emission, and theFINAL_TURN_INSTRUCTIONthat nudges the model to wrap up rather than loop.acp-tool-call.ts- maps internal tool definitions to the ACPToolCall/ToolCallUpdateshape, including theToolKindmapping, follow-alonglocationdata, and the before/after diff for edits.acp-content.ts- converts Nanocoder message blocks (markdown, resource links, embedded resources, fenced code) to and from the ACP content block types.acp-permission.ts- maps tool approval requirements to ACP permission options, respecting the current development mode.acp-question.ts- normalisesask_useroption shapes (string vs{label, value}objects) into strings the editor can render as buttons.acp-capabilities.ts- what we tell the client we support:loadSession,sessionCapabilities.close, plus the fixed list of session mode ids and protocol-version negotiation.acp-types.ts- module-local type aliases.Specs sit next to each file (
*.spec.ts). The total module is around 3.4k lines including tests.JSON-RPC replaces the terminal
In the Ink CLI, conversation state is held inside the React/Ink renderer: a
MessageListkeeps the visible history, aUserInputowns the input box, aToolExecutionCardrenders each in-flight tool call, and the mode/ctx/tune indicators live on a status line. None of that runs over ACP. The renderer is simply not loaded.What does run is the same conversation loop the CLI uses, but it emits to an
AcpAgentadapter instead of to Ink. The adapter translates three things:session/updatenotifications withagentMessageChunkcontent blocks.toolCallnotifications carrying akind(read, edit, delete, move, search, execute, think, fetch, etc.), the touched file paths aslocations, and for edits adiffpayload witholdTextandnewText.requestPermission/ask_usercalls that block on the editor's response.The benefit is concrete: the editor gets to render the agent inside its own UI primitives. Zed, for example, draws the streaming text inside its agent panel, attaches a
diffviewer to each edit tool call, and pops its own allow/deny dialog for permission requests. The Ink equivalent of "a tool card with a diff viewer inside it" is something we would have to design and maintain ourselves; ACP gets it for free as long as the tool call carries adiffblock.What gets rendered in the editor
The current list of affordances that work over ACP, taken from the ACP feature doc and the source:
kind(e.g.edit,read,execute) and the file paths they touch aslocations, so the editor can render a "follow-along" cursor alongside the conversation.string_replaceandwrite_fileadditionally attach adiffblock (oldText,newText) so the editor can show a before/after view in the card itself.normal,auto-accept,yolo,plan- exposed as ACP session modes. Selecting one in the editor is equivalent to pressing Shift+Tab in the CLI. Sessions start inauto-acceptby default.argsinsettings.json(see below).ask_user. When the agent asks a clarifying question, the options appear as selectable buttons in the editor. The model receives whichever option the user picks. Selection only - there is no free-form typed answer over ACP, which is a protocol-level constraint.@-mentioned files. Files referenced through the editor's file-mention UI are sent as resource links and included in the prompt. The editor's live buffer is used when available, so unsaved edits are picked up.What does not work over ACP (yet)
The feature doc lists three honest limitations:
session-history-renderer, but ACP does not yet replay through the protocol. The internalacp-session.tskeeps aMap<sessionId, history>, which is the seam where replay would attach.ask_useris selection-only. ACP permission options have no text input. The model receives the picked option rather than a typed answer. For most clarifying questions this is fine; for anything that needs free-form prose the workaround is to phrase the question so the options cover the meaningful choices.There is also a fourth, unspoken one worth knowing about: the editor spawns the agent in its current working directory, so project-level config (
agents.config.json) and relative paths resolve against the open folder. That is usually what you want. If you open a folder without a configured provider, the agent exits with "No provider configured" and the editor surfaces the error.Tool kinds and follow-along locations
The
ToolKindmapping inacp-tool-call.tsis worth a quick look, because it is what lets the editor draw the right kind of tool card:For file tools, the converter also extracts
locationsso the editor can render a "follow-along" line: opensrc/auth/session.ts, thensrc/auth/session.ts:42, then back. That is the same data the Ink CLI uses to draw path hints, just delivered in a structured field the editor can act on.The diff builder is the more interesting bit. For
string_replace, the converter reads the file off disk, applies the proposedold_str/new_strin memory, and emits:For
write_fileit diffs the existing contents (if any) against the new contents. The editor then renders the diff in the tool card and lets the user step through it before the tool executes - which is to say, permission is reviewed with the proposed change already visible, not after.A subtle detail: the diff builder only synthesises a whole-file diff for
string_replacewhen the replacement is unambiguous. If the sameold_strmatches multiple regions, it skips the synthetic diff rather than guessing. The model gets a normal approval request and the editor gets the raw before/after from the tool args; this avoids the worst failure mode where a "diff" disagrees with what the code actually does.Session modes
acp-capabilities.tsexposes the four development modes as ACP session modes:The mapping is identity today, which is the point. The ACP session mode is just a label the editor flips; the approval pipeline that interprets it is the same one Shift+Tab flips in the CLI. A mid-session change in the editor updates the in-memory development mode and the next approval request goes through the new policy - no special case for ACP.
planis the most useful one to know about here, because the editor integration is where it really starts to earn its keep: switch to plan mode, ask the model to investigate a refactor, get a structured plan back, switch tonormaland ask it to execute, all without leaving the editor. Conversation history carries across the switch because it is just a mode flip on the existing session.Capability negotiation
A short but important detail in
acp-capabilities.ts:The SDK exposes the protocol version; we declare a fixed one. If the editor asks for a newer protocol than we implement, we respond with ours rather than failing the handshake. This avoids the failure mode where a forward-compatible editor feature silently breaks because of a minor version mismatch.
loadSession: trueis the other capability we advertise, alongsidesessionCapabilities.close- that is, we can reload a session by id, and we support the close handshake.Setup in Zed
Zed is the reference ACP client for this release. Two lines in
settings.json(Cmd+, on macOS, orzed: open settingsfrom the command palette):{ "agent_servers": { "Nanocoder": { "command": "nanocoder", "args": ["--acp"] } } }To pin a specific provider or model for the editor session:
{ "agent_servers": { "Nanocoder": { "command": "nanocoder", "args": ["--acp", "--provider", "ollama", "--model", "qwen2.5-coder:7b"] } } }Otherwise Nanocoder resolves the provider and model the same way the CLI does - from the project's
agents.config.json, falling back to the global config. The agent is spawned in the editor's current working directory, so project-level config and relative paths resolve against the open folder.One practical snag worth knowing about: if Zed was launched from the desktop rather than a terminal, it may not see your shell's
PATH. The fix is to pointcommandat an absolute path to the binary (or to the Node runtime plus the script), or to launch Zed from a shell so it inheritsPATH. This is not an ACP issue; it is just how macOS app launches work.ACP vs. the VS Code extension
Both connect Nanocoder to an editor, and both predate v1.28.0 in some form, but they are different mechanisms. The shape of the difference matters for picking which one to use:
--acp--vscodeWith ACP, the editor is the UI. The agent runs headless, and the editor renders the conversation, tool cards, diffs, and approval prompts using its own primitives. The Ink terminal UI is not loaded. The agent does not have its own window to draw in.
With the VS Code extension, the CLI UI stays in charge. You still run
nanocoder --vscodein a terminal; the extension connects over a local WebSocket on port 51820 and adds two things on top: live diff previews for proposed file changes, and active-editor context (the focused file and any selection, pushed to the CLI continuously and rendered as a⊡ In App.tsxpill on the input status line).That difference shows up in three places that matter in practice:
Nanocoder: Start Nanocoder CLI), and the extension connects to the running process.Practically: if your editor supports ACP, that is the cleaner integration today, because the conversation loop is rendering in one place and the editor owns it. The VS Code extension remains the right choice for editors that do not yet speak ACP, and it picks up one nice feature from the v1.28.0 tool consolidation: the
⊡ In <file>status-line pill now points at the slimmed-down file tools and reads more cleanly because there are fewer of them.What is still rough
A few things worth being honest about:
session-history-renderer; the ACP path has the data inacp-session.tsbut does not push it back through the protocol onloadSession. Reopening a thread within a single running agent works; reopening across an editor restart starts empty.ask_userhas no text input. Selection only. For most clarifying questions this is fine; for any question that genuinely needs prose it is a constraint.None of these block day-to-day use. They are the places where the integration gets noticeably less polished if you happen to hit them.
Why this matters
The headline framing of v1.28.0 is "ACP for Zed." The more interesting framing is that Nanocoder now has a second front-end. The conversation loop, tool definitions, provider plumbing, session management, and development modes are all unchanged - the ACP adapter is one more consumer of the same internal APIs the Ink CLI uses. Adding another editor that speaks ACP (there are several in flight upstream of the protocol) is now a matter of writing the JSON-RPC transport, not porting the UI. That is the structural payoff, and it is the reason the module is structured as it is.
If you try it and something is broken or surprising, please open an issue. The Nanocoder repo is the right place, and the ACP-specific docs under
docs/features/acp.mdtrack the current state of what is and is not supported.Beta Was this translation helpful? Give feedback.
All reactions