Skip to content

[Feature] Add Agent Client Protocol (ACP) Support #401

@will-lamerton

Description

@will-lamerton

Summary

Implement Agent Client Protocol support so nanocoder can be used as a coding agent inside any ACP-compatible editor — Zed, JetBrains IDEs, Neovim (Code Companion), Kiro, Emacs, and others — without editor-specific integrations.

What is ACP?

ACP is an open standard by Zed Industries (Apache 2.0) that standardizes how code editors communicate with AI coding agents. It's often called "the LSP for AI agents." The protocol uses JSON-RPC 2.0 over stdin/stdout and covers session management, streaming responses, tool approval, and file/terminal delegation.

Already adopted by: Claude Code, Gemini CLI, Codex CLI, GitHub Copilot CLI, Cline, Goose, OpenHands, and 20+ other agents.

Motivation

Today, nanocoder runs as a standalone CLI or via a custom VS Code extension. Adding ACP support would:

  • Unlock every ACP editor at once — one integration instead of per-editor plugins
  • Let editors manage permissions natively — tool approval flows through the editor's own UI rather than the terminal
  • Enable auto-discovery — editors like JetBrains and Zed can detect nanocoder from the ACP agent registry
  • Align with industry direction — ACP is becoming the standard way editors integrate coding agents

Current Architecture Overlap

Nanocoder already has the core primitives ACP needs:

ACP Concept Nanocoder Equivalent
Sessions Session save/resume (/session commands)
Tool discovery + execution ToolManager unified registry
Human-in-the-loop approval Tool confirmation flow in useToolHandler
Streaming responses AI SDK streaming callbacks
MCP integration Full MCP client with stdio/http transports
File read/write read_file, write_file, string_replace tools
Terminal execution bash tool with streaming output
Modes normal, auto-accept, plan

The gap is a transport layer — nanocoder needs a JSON-RPC stdin/stdout entry point and ACP method handlers.

Proposed Approach

Entry Point — Auto-Detection

Rather than requiring a --acp flag, nanocoder should auto-detect ACP mode. When an editor spawns nanocoder, stdin won't be a TTY — and the first message will be a JSON-RPC initialize request. Nanocoder can detect this and switch to headless ACP server mode automatically.

Startup logic:
1. stdin is not a TTY?
2. First bytes are a JSON-RPC initialize request?
   → Yes: Enter ACP mode (no Ink UI, JSON-RPC transport)
   → No:  Normal CLI mode (Ink UI)

This means ACP works out of the box — editors just spawn nanocoder and start sending JSON-RPC, no special flags needed.

ACP Protocol Lifecycle

Editor                              Nanocoder (ACP mode)
  │                                      │
  │──── initialize ─────────────────────>│  Capabilities + protocol version
  │<─── response ────────────────────────│
  │                                      │
  │──── session/new ────────────────────>│  Working dir, MCP config
  │<─── session ID ──────────────────────│
  │                                      │
  │──── session/prompt ─────────────────>│  User message
  │<─── session/update (streaming) ──────│  Text chunks, tool calls, plans
  │<─── session/request_permission ──────│  Tool approval requests
  │──── permission response ────────────>│
  │<─── PromptResponse ─────────────────│  Final result

New Files

source/acp/
├── acp-server.ts           # JSON-RPC stdin/stdout transport
├── acp-handler.ts          # Method dispatch (initialize, session/*)
├── acp-session.ts          # Session state management
├── acp-capabilities.ts     # Capability negotiation
└── acp-types.ts            # Protocol types

Prerequisite: Headless Service Extraction

The biggest blocker is that core logic currently lives in React hooks (useChatHandler, useToolHandler, useAppInitialization). For ACP mode to work without React/Ink, this logic needs extracting into plain service classes that both the hooks and ACP handler can call.

source/services/
├── chat-service.ts         # Extracted from useChatHandler
├── tool-service.ts         # Extracted from useToolHandler
├── init-service.ts         # Extracted from useAppInitialization
└── session-service.ts      # Extracted from session commands

The existing hooks would become thin wrappers around these services.

Implementation Phases

Phase 1: Transport & Handshake

  • Add @agentclientprotocol/sdk dependency
  • Create stdin/stdout JSON-RPC transport
  • Implement initialize with capability negotiation
  • Add auto-detection logic at startup (non-TTY stdin + JSON-RPC initialize)

Phase 2: Session & Prompting

  • Implement session/new and session/prompt
  • Map AI SDK streaming callbacks to ACP session/update notifications
  • Implement session/cancel via AbortController

Phase 3: Permission Model

  • Implement session/request_permission for tool confirmation
  • Map nanocoder modes: auto-accept → auto-approve, normal → permission requests
  • Support allow_once, allow_always, reject_once, reject_always

Phase 4: Editor-Delegated Services

  • Optionally delegate file ops to editor via fs/read_text_file, fs/write_text_file
  • Optionally delegate shell commands via terminal/create, terminal/output, terminal/kill

Phase 5: Registry & Polish

  • Implement session/load for session resume
  • Implement session/set_mode for mode switching
  • Register in the ACP agent registry

User Experience

With ACP, users interact with nanocoder entirely through their editor. Nanocoder runs as a headless background process — there is no terminal UI.

What it looks like per editor

  • Zed — Built-in assistant panel. You type prompts in the same sidebar used for Zed's AI features. Nanocoder appears as a selectable agent.
  • JetBrains (IntelliJ, WebStorm, etc.) — AI Assistant sidebar. Nanocoder shows up alongside other agents in the tool window. Chat, diffs, and approvals all happen in the IDE.
  • Neovim — Via the Code Companion plugin, which provides a chat buffer and inline diff display for ACP agents.
  • Kiro / Emacs / others — Each has its own ACP client UI, but the pattern is the same: a chat panel where you type, and native UI for tool approvals and file diffs.

What the interaction flow feels like

  1. You type a prompt in the editor's AI chat panel (e.g. "add input validation to the signup form")
  2. Nanocoder streams its response into the chat panel — you see tokens appear in real time
  3. When nanocoder wants to edit a file or run a command, the editor shows a native approval dialog (not a terminal prompt). You click approve/deny or configure always-allow for trusted tools.
  4. File changes appear as diffs in the editor's own diff viewer — you can review, accept, or reject individual edits
  5. Terminal output from bash commands shows in the editor's integrated terminal
  6. The conversation continues — nanocoder sees tool results and keeps working until the task is done

What changes vs the CLI

CLI (current) ACP (in editor)
Where you type Terminal Editor's AI panel
Response rendering Ink.js in terminal Editor's markdown renderer
Tool approval Keyboard confirm in terminal Editor's native dialog/notification
File diffs Shown inline in terminal output Editor's diff viewer with accept/reject
Terminal commands Embedded terminal in Ink Editor's integrated terminal
Session management /session commands Editor manages sessions
Configuration agents.config.json Editor passes config at session/new, falls back to agents.config.json

The core intelligence is identical — same LLM calls, same tools, same MCP servers. Only the interface layer changes.

Open Questions

  • Spec stability — ACP is at v0.11, breaking changes are possible before 1.0. Worth waiting or building against current spec?
  • VS Code extension — ACP would overlap with the existing VS Code extension. Should the extension become an ACP client, or remain as-is for non-ACP editors?
  • File operation model — Start with agent-local file access (simpler) or go straight to editor-delegated (better editor integration)?

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions