Tracking: #264
Status: In progress. The protocol core and app wiring have landed, including
model routing, Settings, MCP/native-tool forwarding, streaming, permissions,
filesystem handling, cancellation, and an in-memory resumable session pool.
See docs/acp-agents.md for current setup. Durable restart
continuity, replay, and cross-client hand-off are now specified separately in
acp-session-continuity.md.
Add Agent Client Protocol (ACP) support to agent-pane so the app can act as an ACP Client and consume external coding agents (Gemini CLI, Copilot CLI, Cline, OpenCode, etc.) instead of only calling LLM APIs directly via the built-in agent loop.
Today agent-pane is both client and agent:
- Main process runs
runAgentLoop, callsLLMProvider.stream(), executes tools viaToolRegistry, persistsllm-history:{threadId}. - Renderer is a UI shell driven by
StreamChunkevents overagent:chunkIPC.
ACP decouples these roles:
| Role | Responsibility |
|---|---|
| Client (agent-pane) | UI, workspace context, filesystem/terminal access, permission prompts |
| Agent (external subprocess) | LLM loop, tool planning/execution, session state |
Transport today is JSON-RPC 2.0 over stdio (client spawns agent subprocess). Remote HTTP/WebSocket is on the roadmap but not stable yet.
- PR #271 (
jkt/auto/remote-agent-chat-7279) adds a Cursor Cloud Agent HTTP/SSE backend (remote-agent:*models). That is complementary but not ACP: it uses Cursor's REST API, not JSON-RPC stdio with bidirectional client callbacks (fs/*,terminal/*,session/request_permission). - ACP and remote Cursor agents can coexist as separate model backends selected in the model picker.
Current:
Renderer → agent:run → agent-service → runAgentLoop → LLMProvider + ToolRegistry
ACP:
Renderer → agent:run → AcpAgentService → ClientSideConnection (stdio) → External Agent
↑
└── agent calls back: fs/*, terminal/*, session/request_permission
External Agent → session/update → adapt → StreamChunk → agent:chunk → Renderer
ACP is not a drop-in LLMProvider. It replaces the entire agent-service → runAgentLoop → ToolRegistry path for a given conversation.
Option A — Parallel backend (recommended)
Route by model prefix:
acp:<agentId>→AcpAgentService- everything else → existing
agent-service
Do not wrap ACP in LLMProvider.stream() — the external agent owns the loop and needs bidirectional JSON-RPC.
src/main/services/acp/
acp-agent-registry.ts # configured agents (command, args, env)
acp-connection-manager.ts # spawn, initialize, lifecycle per agent process
acp-session-store.ts # threadId ↔ acpSessionId, agentId, capabilities
acp-client-handlers.ts # fs, terminal, permission JSON-RPC handlers
acp-update-adapter.ts # session/update → StreamChunk
acp-agent-service.ts # runAgent / abortAgent for ACP threads
src/shared/types/acp.ts # config + session mapping types
Use @agentclientprotocol/sdk (ClientSideConnection for stdio). The repo already spawns MCP servers similarly in src/main/services/mcp-registry.ts.
initialize— negotiate protocol v1, exchange capabilitiesauthenticate— if agent advertisesauthMethodssession/neworsession/resume— withcwd(absolute workspace path) andmcpServersfromuserData/mcp.jsonsession/prompt— user content as ACPContentBlock[]- Stream
session/updatenotifications → adapt toStreamChunk→agent:chunk - Handle agent → client requests:
fs/read_text_file,fs/write_text_file,terminal/*,session/request_permission session/cancelon user abort;session/closeon thread delete (if supported)
| Existing | ACP reuse |
|---|---|
StreamChunk + agent:chunk |
Primary UI adapter target |
src/renderer/controller/agent.ts |
Unchanged if chunks stay compatible |
mcp-registry.ts / mcp.json |
Forward as mcpServers on session create/resume |
approval.ts |
Map session/request_permission |
workspace.ts |
Back fs/read_text_file / path sandboxing |
terminal-service.ts |
Back terminal/* (headless pty sessions) |
model-options.ts |
Add "ACP Agents" optgroup |
| Current behavior | ACP gap |
|---|---|
llm-history:{threadId} |
Agent owns history; store acpSessionId per thread |
write_file → diff queue |
Agent calls fs/write_text_file directly — need intercept policy |
Local ToolRegistry execution |
Disabled in ACP mode; agent runs its own tools |
context_trimmed / subagent explore |
Partial parity; agent-specific |
ACP agents expect fs/write_text_file to write immediately. agent-pane's signature UX is diff approval via diff-queue.ts.
Recommended v1: intercept fs/write_text_file → route through stageDiff() → block JSON-RPC response until user approves/rejects → write on approve.
Make this configurable per agent if some agents break on delayed writes.
Disable local ToolRegistry execution for the parent turn. Forward user MCP servers only; do not double-connect the same MCP server from both agent-pane and the external agent.
| Store | Native mode | ACP mode |
|---|---|---|
llm-history:{threadId} |
LLM transcript | Unused |
threads:{projectId} |
UI state | UI state |
acp-session:{threadId} |
N/A | acpSessionId + agent metadata |
Prefer session/resume when agent advertises sessionCapabilities.resume.
| agent-pane | ACP |
|---|---|
| Plain text | { type: 'text', text } |
| Image attachments | { type: 'image', ... } if agent supports promptCapabilities.image |
@file references |
{ type: 'resource', resource: { uri, text } } if embeddedContext |
Target ACP v1 initially. Isolate adapter behind version check at initialize; v2 RFDs change tool call update semantics.
ACP sessionUpdate |
StreamChunk |
|---|---|
agent_message_chunk |
{ type: 'text', text } |
tool_call |
{ type: 'tool_call', toolCall: { id, name: title, args: rawInput } } |
tool_call_update |
Update tool card status/result |
plan |
New UI (Phase 4) |
thought_chunk |
Activity indicator or new chunk type |
{
"clientCapabilities": {
"fs": { "readTextFile": true, "writeTextFile": true },
"terminal": true
},
"clientInfo": { "name": "agent-pane", "title": "Agent Pane", "version": "0.1.0" }
}interface AcpAgentConfig {
id: string // e.g. "gemini-cli"
title: string // "Gemini CLI"
command: string // absolute path or PATH lookup
args: string[]
env?: Record<string, string>
enabled: boolean
authMethodId?: string
}Model values: acp:gemini-cli, acp:copilot-cli, etc.
Settings UI: new ACP Agents section — add/edit agents, test initialize, optional auth flow. Consider ACP Registry later.
In src/main/index.ts:
const model = getSetting('model')
if (model.startsWith('acp:')) {
await runAcpAgent(threadId, userContent, ...)
} else {
await runAgent(threadId, userContent, ...) // existing
}- Add
@agentclientprotocol/sdk - Hardcode one agent (e.g. Gemini CLI)
initialize→session/new→session/prompt→ stream text to UI- Exit: text-only Q&A end-to-end
fs/read_text_file,fs/write_text_file(with diff intercept)session/request_permission→ approval dialogsession/cancel+ abort button- Model routing
acp:*
terminal/*handlersacp-session:{threadId}+session/resume- MCP forwarding from
mcp.json - Settings UI for agent CRUD
- Agent registry, connection pooling
authenticate/logout- Session config options, usage reporting
- Plan updates, slash commands, modes
session/list/session/delete- E2E with mock ACP agent subprocess
- ACP Registry integration
| Layer | Approach |
|---|---|
| Unit | acp-update-adapter.test.ts — fixture JSON → StreamChunk[] |
| Unit | Client handlers — path sandboxing, diff intercept, permissions |
| Integration | SDK example agent or minimal mock over stdio |
| E2E | Mock agent in WDIO; screenshots for tool cards |
| Risk | Mitigation |
|---|---|
| Write intercept breaks agents | Per-agent policy: direct vs diff approval |
| Agent stderr on stdio | Surface stderr in log panel; strict stdout discipline |
| Session divergence | Store acpSessionId immediately; prefer session/resume |
| v2 protocol churn | Version gate at initialize |
| Subprocess zombies | Connection manager + idle timeout + session/close on quit |
- User selects an ACP agent in the model dropdown and chats (agent brings its own auth).
- External agent reads workspace files and proposes edits with agent-pane diff approval.
- Tool calls render with human-readable labels; cancel works mid-turn.
- Multi-turn threads resume after restart when agent supports it.
- Native Anthropic/OpenAI/LM Studio path remains unchanged.
@agentclientprotocol/sdkdependencyacp-connection-manager.ts+acp-update-adapter.ts- One hardcoded agent behind
acp:geminimodel - Text streaming +
session/cancelonly - Feature flag / hidden setting
Defer fs/terminal/permission to the follow-up PR.