Editor & IDE Compatibility
One Core, Many Surfaces
An agent harness is not a CLI. It is not a VS Code extension. It is not a web service. It is a core runtime — conversation loop, tool registry, permission model, configuration — that can be presented through any of these interfaces. The interface is a thin translation layer between the core’s event stream and whatever medium the user is working in.
This separation is not optional for any agent that intends to reach users where they work. Developers use terminals, VS Code, JetBrains IDEs, Vim/Neovim, web editors, and increasingly mobile interfaces. An agent that only works in one of these is an agent that most developers cannot use in their primary workflow.
The Interface Matrix
Each surface has distinct constraints on transport, input handling, output rendering, and lifecycle management.
| Interface | Transport | Input | Output | Lifecycle |
|---|---|---|---|---|
| CLI / REPL | stdin/stdout | Line editing (readline/rustyline), signal handling (Ctrl+C, Ctrl+D) | Streaming markdown, syntax highlighting, terminal width adaptation | Process lifetime = session lifetime |
| VS Code | JSON-RPC over stdio or WebSocket | Extension API messages, editor selections, file context | Webview panels, inline annotations, editor decorations, progress indicators | Extension host manages lifecycle; survives editor restarts via state serialization |
| JetBrains | HTTP/WebSocket or stdio | Plugin API messages, PSI tree context, editor selections | Tool windows, editor inlays, notification balloons | JVM plugin lifecycle; must handle project open/close events |
| Neovim | stdio or TCP (via Lua RPC) | Lua callbacks, buffer context, visual selections | Floating windows, virtual text, quickfix lists | Neovim process lifetime; plugin lazy-loaded on demand |
| Web | HTTP/SSE or WebSocket | REST API requests, WebSocket messages | JSON event stream consumed by frontend framework | Stateless server; session state externalized to database or cache |
The Harness Event Stream
The core runtime should emit a single, interface-agnostic event stream that every surface consumes and translates into its native presentation.
graph LR
subgraph core ["Agent Core"]
CL["Conversation Loop"]
TR["Tool Registry"]
PM["Permission Model"]
end
ES["Event Stream"]
subgraph surfaces ["Interface Surfaces"]
CLI["CLI / REPL"]
VSC["VS Code Extension"]
JB["JetBrains Plugin"]
WEB["Web Server"]
end
core --> ES
ES --> CLI
ES --> VSC
ES --> JB
ES --> WEB
Event Types
The event stream defines a vocabulary that is rich enough to drive any interface but contains no interface-specific concerns:
| Event | Payload | CLI renders as | IDE renders as | Web emits as |
|---|---|---|---|---|
MessageStart | { role, turn_id } | Newline + role prefix | New chat bubble in panel | SSE message_start |
TokenDelta | { text } | Append to terminal output | Append to webview panel | SSE token_delta |
ToolCallRequest | { tool, args, requires_approval } | Print tool name + args | Show inline annotation at cursor | JSON tool_request |
PermissionPrompt | { tool, args, risk_level } | Interactive terminal prompt (y/n) | Modal dialog with approve/deny buttons | JSON requiring frontend confirmation |
ToolCallResult | { tool, status, content } | Print result, syntax-highlighted | Collapsible panel in chat view | SSE tool_result |
Error | { code, message } | Print to stderr in red | Error notification balloon | SSE error |
SessionEnd | { reason, summary } | Print summary + exit | Update status bar, offer to resume | SSE session_end |
The key principle: the core never imports terminal, editor, or HTTP libraries. It emits events. The surface translates.
Permission Prompts Across Surfaces
Permission handling is the hardest interface problem. In the CLI, a permission prompt is a blocking readline call. In an IDE, it is a non-blocking dialog. In a web app, it is an asynchronous HTTP round-trip. The core must support all three.
sequenceDiagram
participant C as Agent Core
participant S as Interface Surface
C->>S: PermissionPrompt { tool: "bash", args: "rm -rf dist/", risk: "destructive" }
Note over S: CLI: blocking readline<br/>IDE: modal dialog<br/>Web: async HTTP
alt User approves
S->>C: PermissionResponse { approved: true }
C->>C: Execute tool
else User denies
S->>C: PermissionResponse { approved: false }
C->>C: Return denial to model
end
Design Requirements
- The core must not block on permission prompts. It emits the event and awaits a response asynchronously. The surface decides how to collect the user’s decision.
- Timeouts are surface-specific. A CLI might auto-deny after 60 seconds. An IDE might keep the dialog open indefinitely. A web app might expire the prompt after 5 minutes.
- Batch approvals are surface-specific. An IDE might offer “approve all
bash(git *)for this session.” The core only sees individual approval/denial responses; the surface manages batching.
Core Extraction Pattern
The architectural pattern for achieving interface independence is core extraction: the agent’s logic is built as a library with no I/O dependencies, and each interface surface is a thin binary or plugin that wires the library to its environment.
agent-core/ # Library crate/package — no I/O, no UI
conversation.rs # Agentic loop, tool dispatch
tools.rs # Tool registry, execution
permissions.rs # Permission model, approval flow
config.rs # Configuration loading, merging
events.rs # Event type definitions
agent-cli/ # Binary — depends on agent-core
main.rs # REPL, terminal rendering, signal handling
agent-vscode/ # Extension — depends on agent-core (via WASM or subprocess)
extension.ts # VS Code API bindings, webview panels
agent-jetbrains/ # Plugin — depends on agent-core (via subprocess or HTTP)
Plugin.kt # IntelliJ API bindings, tool windows
agent-server/ # HTTP server — depends on agent-core
server.rs # REST/SSE endpoints, session management
Testing the Core Without an Interface
If the core is properly extracted, it can be tested with a mock surface that records events and replays permission responses:
core = AgentCore(config, tools)
mock_surface = MockSurface(
permission_responses={"bash": True, "write_file": False}
)
events = core.run(
message="Delete the dist folder and recreate it",
surface=mock_surface
)
assert any(e.type == "PermissionPrompt" and "rm" in e.args for e in events)
assert any(e.type == "ToolCallResult" and e.tool == "bash" for e in events)
assert not any(e.type == "ToolCallResult" and e.tool == "write_file" for e in events)
If your tests require a terminal emulator or a browser, your abstraction is leaking.
Cross-Platform Adoption
| Platform | CLI | VS Code | JetBrains | Web | Architecture |
|---|---|---|---|---|---|
| Claude Code | Native REPL | First-party extension | First-party extension | claude.ai/code | Core library + thin shells per surface |
| OpenAI Codex | Native CLI | ChatGPT desktop (limited) | Not available | Not available | CLI-first, limited surface coverage |
| Gemini CLI | Native CLI | Not available | Android Studio integration (Gemini) | AI Studio | Separate products, not shared core |
| Cursor | Not available | Fork of VS Code (native) | Not available | Not available | Single-surface (VS Code fork) |
| Windsurf | Not available | Fork of VS Code (native) | Not available | Not available | Single-surface (VS Code fork) |
The trend is clear: agents that start as single-surface products (Cursor, Windsurf) face increasing pressure to support additional interfaces as users expect the same agent in their terminal, their IDE, and their browser. Agents that extract the core early (Claude Code) can expand to new surfaces without rewriting the agent logic.
State Synchronization
When the same agent core serves multiple surfaces simultaneously (e.g., a user has both the CLI and VS Code open), session state must be synchronized or explicitly isolated.
| Strategy | How It Works | Tradeoff |
|---|---|---|
| Exclusive sessions | Each surface owns its session. No sharing. | Simple. Users lose continuity when switching surfaces. |
| Shared server | A local daemon process runs the agent core. CLI and IDE connect as clients. | Full continuity. Adds a daemon lifecycle to manage. |
| State file locking | Session state serialized to disk. Surfaces acquire a lock before reading/writing. | Works offline. Lock contention possible. |
| Cloud sync | Session state stored in a remote service. All surfaces read/write via API. | Works across machines. Requires network. Adds latency. |
The shared-server pattern is emerging as the dominant approach for local development: a single agent-server process manages sessions, and both the CLI and IDE extension connect to it as clients. This avoids duplicate model calls and ensures that a tool call initiated in VS Code is visible in the terminal.
Key Takeaways
- An agent harness is a core runtime plus interface surfaces. Build the core as a library with no I/O. Build each surface as a thin translation layer.
- Define a single event stream vocabulary (
MessageStart,TokenDelta,PermissionPrompt,ToolCallResult, etc.) that every surface consumes. - Permission prompts are the hardest interface problem. The core must be asynchronous; blocking behavior is a surface concern.
- Agents that extract the core early can expand to new surfaces without rewriting logic. Agents that embed logic in the interface are locked to one surface.
- Test the core without any interface. If your test setup requires a terminal or browser, refactor until it doesn’t.
- For multi-surface environments, a local daemon architecture avoids duplicate sessions and keeps state consistent across CLI and IDE.