A coding agent rarely stays inside one interface.
A developer may want an interactive terminal for exploration, a browser for reviewing tool calls and Git changes, a desktop workspace for editing, a one-shot command for CI, a persistent process for an SDK, or an IDE connection through the Agent Client Protocol (ACP).
The easy implementation is to build a separate agent for each surface. The result is usually six versions of model selection, permissions, sessions, tools, and error handling that slowly drift apart.
SolonCode takes a different route. Its public Java source exposes six user-facing surfaces—interactive CLI, Web, Desktop, one-shot run, persistent stream, and ACP—backed by five explicit CLI modes plus the interactive fallback, all built around the same Agent composition.
That does not mean the six interfaces are identical. It means their product-specific adapters sit around a shared core instead of reimplementing the coding agent each time.
The architecture in one picture
At a high level, the design looks like this:
Interactive CLI ─┐
Web UI ──────────┤
Desktop ─────────┤
run ─────────────┼──> WorkspaceContext ──> HarnessEngine
stream ──────────┤ │ │
ACP ─────────────┘ │ ├─ models
│ ├─ tools
├─ sessions ├─ skills / agents
├─ loop scheduler ├─ permissions
├─ file watcher └─ memory
└─ workspace settings
The important class is not a UI component. It is the workspace context assembled by the Java backend.
In Configurator.agentRuntime(...), SolonCode initializes the default workspace, asks WorkspaceManager for that context, captures the context's Loop scheduler, and returns the context's HarnessEngine. The command dispatcher then connects the selected entry path to that runtime.
There is an important precision here: this is a shared composition model, not a promise that every workspace in every process uses one JVM-global engine. SolonCode Web supports workspace-specific contexts. The architectural point is that those contexts are built through the same runtime path rather than each interface inventing its own agent stack.
1. Interactive CLI: the direct conversation adapter
With no special mode selected, SolonCode starts CliShell on an interactive thread.
soloncode cli
This is the most direct product surface: prompt, streaming response, tool activity, commands, Skills, and session continuity in a terminal. It is useful when the terminal is already the developer's main control plane.
The CLI adapter owns terminal behavior—welcome text, keyboard interaction, command completion, and rendering—but it does not need its own model registry or separate tool implementation. Those remain runtime concerns.
That separation matters for maintenance. Adding a tool to the runtime should not require rebuilding the tool six times just because users can reach it from six places.
2. Web: a richer view over workspace state
The Web entry path is selected with:
soloncode web 0
Passing 0 asks SolonCode to probe for an available ephemeral port. Unlike the default CLI path, Web mode explicitly enables HTTP and WebSocket support. It registers the Web gate and the controllers for chat, settings, Skills, models, MCP, OpenAPI, LSP, memory, authentication, and remote runs.
This is a good example of an adapter adding product value without replacing the core. The browser can provide things a terminal cannot express as comfortably:
- file-tree navigation;
- structured tool cards;
- model and provider settings;
- Skills management;
- session controls;
- Git status and diff review;
- Loop task configuration;
- real-time updates over WebSocket.
The browser is not “the agent.” It is a client and control surface around workspace-aware Agent execution.
The source also avoids turning HTTP into an accidental dependency of every mode. App disables HTTP by default and enables it only for modes that need it, such as web and serve. ACP explicitly keeps HTTP and WebSocket disabled because its transport is standard input/output.
That is a small but valuable boundary: a headless run should not quietly start a server just because the same application can also serve a browser UI.
3. Desktop: an IDE surface, not a second Agent backend
SolonCode Desktop combines a Tauri/React interface with the Java CLI backend.
For development, the documented flow starts the backend separately:
soloncode serve 4808
The Desktop client then connects over HTTP and WebSocket. Its UI adds Monaco editing, an integrated terminal, file operations, Git controls, local workspace management, and native application behavior.
The responsibilities are deliberately split:
React / TypeScript UI
├─ layout and editor UX
├─ Agent conversation UI
└─ HTTP + WebSocket client
Tauri / Rust native layer
├─ filesystem operations
├─ integrated terminal
└─ CLI process management
Java CLI backend (`soloncode serve`)
├─ Agent runtime
├─ models and tools
├─ sessions
├─ Skills and memory
└─ workspace execution
Desktop therefore does not need to ship a second implementation of reasoning, tool permission rules, or model invocation. It specializes in the IDE experience.
This is also why “same core” should not be misread as “same UI.” Web and Desktop can expose different controls, persistence choices, and editing affordances while still delegating Agent work to the same backend architecture.
4. run: one prompt, one lifecycle, a useful exit code
Interactive products are only half the story. Automation needs a command that completes one task, can emit a machine-readable result, and exits with a meaningful status code.
That is the role of soloncode run:
soloncode run "Inspect this repository and return the three highest-risk modules" \
--permission-mode plan \
--output-format json \
--max-turns 8
PrintMode makes the lifecycle explicit. It resolves a prompt from the argument or standard input, applies runtime options, resolves a session, runs the Agent stream to completion, renders the chosen output, and returns an explicit exit code.
The source distinguishes at least these outcomes:
0 success
1 execution error
2 maximum turns exceeded
3 missing prompt
4 budget exceeded
This makes run suitable for shell scripts and CI jobs because success is not inferred from a sentence in a chat transcript.
It also supports three output styles:
-
textfor a human-readable final answer; -
jsonfor one result object; -
stream-jsonfor JSONL output during one run; initialization and final-result events are always emitted, while intermediate assistant and tool events require--verbose.
The last item needs careful wording. Streamed output does not make run a persistent multi-turn process. The implementation rejects persistent stream input in run and tells callers to use the dedicated stream command instead.
5. stream: a process that stays alive
soloncode stream is a different lifecycle, not an alias for run.
soloncode stream --verbose
It is designed for a parent process or SDK that communicates through JSONL while keeping the Agent process alive across turns. A persistent consumer can reuse context without forking a new Java process for every message.
This separation avoids an ambiguous command whose behavior changes depending on a hidden combination of flags:
run = one task, then exit
stream = persistent JSONL input/output channel
That distinction is especially useful for integration code. A CI step normally wants the determinism of run; an SDK bridge normally wants the process lifetime of stream.
The shared runtime still matters. Persistent transport does not require a second tool catalog or a reduced “automation agent.” It changes how prompts and events cross the process boundary.
6. ACP: translate Agent events instead of rebuilding the Agent
ACP is the protocol-facing entry path:
soloncode acp
AcpLink creates an ACP agent over standard input/output. When an ACP prompt arrives, it converts the request into a Solon AI Prompt, obtains an Agent session, and runs the request through the injected runtime's prompt–session–stream pipeline.
Then it translates runtime events into protocol updates:
- plan events become ACP plan entries;
- reasoning deltas can become thought updates;
- tool starts become in-progress tool-call cards;
- tool completions become completed or failed updates;
- file edits can become structured diffs;
- the final run event becomes the final ACP message.
This is adapter code in the best sense. ACP defines how an IDE or client sees the work, while HarnessEngine defines how the work is done.
Not every internal event is exposed. SolonCode filters internal task-dispatch, memory, and Goal tools from ACP tool cards. That is another sign of a deliberate boundary: sharing a runtime does not mean leaking every internal mechanism into every protocol.
What is actually shared?
“Shared core” can become vague architecture marketing, so it helps to name the concrete concerns.
Model selection and invocation
The adapters do not each implement provider calls. They select or pass model context into the Agent runtime.
Tools and permissions
File reads, edits, search, terminal execution, Web access, and extension tools belong to the runtime. Entry paths can change permission behavior—for example, a headless plan run denies write-oriented tools—but they apply that policy to the same tool system.
Sessions
Each adapter resolves sessions in a way appropriate to its lifecycle. An ACP session is not automatically the same identifier as a Web session, and a one-shot run may create its own print session. What is shared is the session abstraction and Agent execution path, not magical cross-client session synchronization.
Skills, agents, MCP, and memory
These capabilities are composed into the engine and workspace. An adapter can expose management UI or choose a stripped-down mode such as --bare, but it does not need to invent an incompatible extension model.
Workspace boundaries
The workspace context brings together the engine and workspace services. Web can manage more than the default context, which is why the safest architecture statement is “one composition path per workspace,” not “one global Agent object for everything.”
What remains adapter-specific?
A clean shared core does not erase product differences.
| Entry path | Adapter-specific concern |
|---|---|
| Interactive CLI | terminal rendering, keyboard flow, slash commands |
| Web | HTTP/WebSocket, browser state, settings panels, Git and task UI |
| Desktop | native window, Monaco, terminal, local file and Git experience |
run |
one-shot lifecycle, output format, exit status, automation options |
stream |
persistent JSONL transport and turn interruption |
| ACP | protocol capabilities, session requests, structured plan/tool updates |
That table is the design in practical terms: reuse the Agent, specialize the boundary.
Why this architecture is useful to users
Most users will never open Configurator.java, but they still feel the consequences of the design.
A feature can travel across surfaces
When a capability belongs to the core, a new client does not have to start from zero. It still needs adapter work and UI, but the model, tool, Skill, permission, and workspace foundations already exist.
Automation is not a fragile scrape of the UI
run, stream, and ACP are first-class entry paths. A script does not need to drive browser buttons, and an IDE does not need to parse colored terminal output.
Interfaces can stay focused
The CLI can remain fast and direct. Web can prioritize review and settings. Desktop can prioritize editing. ACP can prioritize protocol fidelity. They do not all need to become the same giant application.
Runtime behavior is easier to audit
The public dispatch path is visible in one place. A reviewer can trace which command enables HTTP, which adapter receives the runtime, and how protocol events are mapped.
A practical way to choose an entry path
Use the interface that matches the lifecycle of the work:
- Choose interactive CLI when you are living in a terminal and want a direct conversation.
- Choose Web when you want browser-based review, settings, sessions, Git visibility, and task controls.
- Choose Desktop when Agent work should sit next to editing, files, terminal, and Git in one native workspace.
- Choose
runwhen a script or CI step has one bounded task and needs a result plus exit code. - Choose
streamwhen a parent process needs a persistent JSONL conversation. - Choose ACP when an ACP-compatible client or IDE should receive structured plans, tool calls, diffs, and results.
The choice changes the interaction contract. It does not require choosing a different SolonCode Agent implementation.
The broader engineering lesson
Multi-interface Agent products have two bad extremes.
The first is duplication: every client builds its own Agent stack, and behavior drifts. The second is false uniformity: every client is forced through one UI-shaped API even when a terminal, CI process, desktop editor, and protocol client have different needs.
SolonCode's source shows a more useful middle path:
- compose the Agent at the workspace layer;
- keep models, tools, permissions, sessions, Skills, and memory in that core;
- give each entry path a lifecycle-specific adapter;
- enable network servers only where they are needed;
- translate runtime events into the native language of each surface.
The result is not six identical products. It is one open Agent architecture with six honest boundaries.
Source reviewed: https://github.com/opensolon/soloncode
Key implementation paths:
soloncode-cli/src/main/java/org/noear/solon/codecli/Configurator.javasoloncode-cli/src/main/java/org/noear/solon/codecli/App.javasoloncode-cli/src/main/java/org/noear/solon/codecli/portal/printmode/PrintMode.javasoloncode-cli/src/main/java/org/noear/solon/codecli/portal/acp/AcpLink.javasoloncode-desktop/README.md



Top comments (0)