In-app agent chat: your own coding agents over ACP, plus OpenAI-compatible endpoints (#1102)
* Agent chat prototype: drive an ACP agent the user already has from a side panel A desktop-only chat panel (off by default) that starts a coding agent over the Agent Client Protocol and gives it SolidCraft's MCP server, bridged to the open window through the token-protected control channel with the default folder limits. It finds installed agents (Claude Code and Codex adapters, Gemini CLI, Copilot, Qwen, OpenCode, goose) with version and login state, streams text, tool calls and plans, shows the agent's permission requests, and when an agent isn't signed in shows its own login methods without handling any credential. Design notes and findings: docs/design/in-app-agent-chat.md. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: say only what the end-to-end run showed Co-Authored-By: Claude Opus 5.5 <[email protected]> * Agent chat: gate, one undo per message, docking, process groups, discovery polish Co-Authored-By: Claude Opus 5.5 <[email protected]> * Agent chat: Settings → AI, several agents with a switcher, OpenAI-compatible endpoints, permission modes, toolbar button Co-Authored-By: Claude Opus 5.5 <[email protected]> * Agent chat: translations, tool cards, Add agent dialog fixes, test agent saves on its own Co-Authored-By: Claude Opus 5.5 <[email protected]> * Agent chat: third-party licences for the keychain crates; clippy Co-Authored-By: Claude Opus 5.5 <[email protected]> * Agent chat: badge text from the theme tokens Co-Authored-By: Claude Opus 5.5 <[email protected]> * Agent chat: Auto-allow model edits is the default permission mode Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
02fe495d38
commit
2d069ca186
36 files changed
+8078
-303
No files matched your search
@@ -0,0 +1,181 @@
|
||||
# Agent chat
|
||||
|
||||
Agent chat is a side panel where you talk to a coding agent you already use (Claude Code,
|
||||
Codex, Gemini CLI, OpenCode and others), or to a model behind an OpenAI-compatible endpoint you
|
||||
choose (Ollama or LM Studio on your computer, OpenAI, any other URL), and have it work on the
|
||||
design that's open. A coding agent runs on your computer under **your own account** with that
|
||||
agent: SolidCraft doesn't sell or proxy a model and needs no key for it. An endpoint's key, if
|
||||
it needs one, goes into your system keychain, never into SolidCraft's settings.
|
||||
|
||||
Desktop app only.
|
||||
|
||||
## Turning it on and off
|
||||
|
||||
Everything about it is on one page: **Preferences → AI**.
|
||||
|
||||
- **Show AI features** (on by default) is the master switch. Turned off, SolidCraft shows nothing
|
||||
of it: no AI button, no shortcut, no menu entry, no panel, and nothing is looked for or
|
||||
started. The page stays, so you can turn it back on.
|
||||
- **Show button in toolbar** (on by default): the **AI** button at the right end of the toolbar
|
||||
row, which stays there however wide the window is. Turn it off to keep only the shortcut and
|
||||
the menu entry.
|
||||
|
||||
With AI features on, the panel opens from:
|
||||
|
||||
- the **AI** button at the right end of the toolbar;
|
||||
- **Ctrl+Shift+A** (Cmd+Shift+A on a Mac), which also works from the chat's own text box;
|
||||
- **View settings** (the last button of the navigation bar under the view) → **Agent chat**.
|
||||
|
||||
Nothing runs until you need it: SolidCraft looks for installed agents (a quick `--version`
|
||||
each, in the background) when the panel or the AI page first opens, and starts an agent only
|
||||
when you send a message or press **Connect**.
|
||||
|
||||
## The panel
|
||||
|
||||
- **Docking:** drag the panel's header (the dotted grip and the title) to the other half of the
|
||||
window and let go. The side it would land on lights up while you drag. You can also pick
|
||||
**Left** or **Right** under Preferences → AI → Panel. SolidCraft remembers the side.
|
||||
- **Width:** drag the panel's inner edge. SolidCraft remembers it.
|
||||
- **Minimise** (the line in the header) folds the panel into a thin strip that keeps the chat
|
||||
icon, the agent's state, and a count of anything waiting for your answer. Click the icon to
|
||||
unfold it. **Close** (×) hides the panel. Neither stops the agent; **Stop** does.
|
||||
- **The agent switcher** under the title picks the agent for this conversation; switching stops
|
||||
the one running. Next to it, when the agent has modes of its own (Claude Code has *Default*,
|
||||
*Accept Edits*, *Plan*…), a second drop-down sets the agent's mode.
|
||||
|
||||
## Agents
|
||||
|
||||
Preferences → AI → **Agents** lists every agent the chat can use, each with its kind (a program or
|
||||
an endpoint), version or model, whether it looks signed in, **Test**, **Default** (the one a new
|
||||
conversation starts with), its own permission setting, and **Remove** for the ones you added.
|
||||
|
||||
### Found by themselves
|
||||
|
||||
SolidCraft looks for agents that speak the [Agent Client Protocol](https://agentclientprotocol.com)
|
||||
on your `PATH` and in the usual install folders a desktop app's `PATH` often misses (npm's
|
||||
global folder, nvm, Volta, bun, `~/.local/bin`, Homebrew, `%APPDATA%\npm`). For each one it
|
||||
shows its **version** (from `--version`, given three seconds) and whether it looks **signed in**.
|
||||
SolidCraft only checks whether the agent's own login file (`~/.claude/.credentials.json`,
|
||||
`~/.codex/auth.json`, `~/.gemini/oauth_creds.json`, …) or one of its key variables exists. It never
|
||||
reads the file and never starts the agent to find out. On macOS Claude Code keeps its login in
|
||||
the keychain, so SolidCraft doesn't guess there.
|
||||
|
||||
| Agent | What to install |
|
||||
|---|---|
|
||||
| Claude Code | `npm install -g @agentclientprotocol/claude-agent-acp` (the ACP adapter for Claude Code) |
|
||||
| Codex | `npm install -g @agentclientprotocol/codex-acp` |
|
||||
| Gemini CLI | `npm install -g @google/gemini-cli` (it speaks ACP itself: `gemini --acp`) |
|
||||
| OpenCode | `npm install -g opencode-ai` (`opencode acp`) |
|
||||
| GitHub Copilot CLI, Qwen Code, goose | listed with their install command when missing |
|
||||
|
||||
The panel lists agents that aren't installed with their install command and a **Copy** button.
|
||||
**Refresh** looks again after you install one. **Test** starts an agent briefly, asks it for a
|
||||
session and ends it again.
|
||||
|
||||
### Add agent…
|
||||
|
||||
- **Installed agent:** your own entry for an installed agent, for example with other arguments.
|
||||
- **Custom command:** any other program that speaks ACP on stdin and stdout, such as
|
||||
`my-agent --acp`. SolidCraft starts it directly, not through a shell.
|
||||
- **OpenAI-compatible API:** a preset (Ollama `http://localhost:11434/v1`, LM Studio
|
||||
`http://localhost:1234/v1`, OpenAI) or a custom URL, the model name, and an optional key.
|
||||
The key goes into the system keychain (Keychain on macOS, Credential Manager on Windows, the
|
||||
Secret Service on Linux) and is only ever sent over HTTPS or to this computer. **Test** asks the
|
||||
endpoint for its models. For these agents SolidCraft runs the conversation itself, with the
|
||||
same tools, limits and questions as for a coding agent, and at most 24 model requests per
|
||||
message.
|
||||
|
||||
## Signing in
|
||||
|
||||
If a coding agent isn't signed in, it says so and the panel shows **Sign in to the agent** with
|
||||
the agent's own ways to sign in:
|
||||
|
||||
- **A command for a terminal**, e.g. `claude-agent-acp --cli auth login --claudeai`, with a **Copy**
|
||||
button. Run it in a terminal, sign in there, then press **I've signed in: connect again**.
|
||||
- **A button** for agents that sign in by themselves (Codex's ChatGPT login opens your browser).
|
||||
- **A variable to set** for agents that take a key from the environment. The panel has no field
|
||||
for a coding agent's key, on purpose.
|
||||
|
||||
Whatever you typed before signing in is sent once the agent is ready.
|
||||
|
||||
## What the agent can do
|
||||
|
||||
The agent reaches the open design only through SolidCraft's own tools (the
|
||||
[MCP server](mcp.md)), which SolidCraft hands it when the chat starts:
|
||||
|
||||
- **Read the design:** list commands, inspect features, sketches and parameters, measure,
|
||||
take pictures of the view.
|
||||
- **Change the design:** run any modelling command, alone or in a batch. Changes show in the
|
||||
window as they happen.
|
||||
- **One undo per message:** everything the agent changes while answering one message becomes
|
||||
a single undo step, named after your message. The conversation shows *N changes, one undo
|
||||
step* with **Undo these changes** while nothing else has happened since.
|
||||
- **Tool cards:** each tool call shows as a card with its state; click it to see what it returned.
|
||||
- **Attach view:** tick it to send a picture of the current view with your message, for agents
|
||||
that take pictures.
|
||||
|
||||
## Permissions
|
||||
|
||||
Preferences → AI → **Permissions** decides what the agent may do without asking, for every agent;
|
||||
each agent can have its own setting in its row.
|
||||
|
||||
- **Ask every time:** every change to the design and every file action waits for **Allow**,
|
||||
**Always allow** or **Deny** in the panel.
|
||||
- **Auto-allow model edits** (the default): modelling commands run at once; saving, opening, exporting,
|
||||
importing, starting a new design, any command given a file name, and undoing past the start of
|
||||
your message still ask.
|
||||
- **Allow everything:** nothing asks. The page warns you: the agent can then save, overwrite and
|
||||
replace your design.
|
||||
|
||||
**Always allow** remembers that command for that agent. The page lists what you allowed this way,
|
||||
per agent, with **Reset**.
|
||||
|
||||
These settings cover SolidCraft's tools. A coding agent's own tools (its shell, its file edits)
|
||||
follow the agent's own mode, which you pick in the panel's header. When an agent asks before
|
||||
using one of SolidCraft's tools, SolidCraft answers for you, since its own gate checks the call,
|
||||
and notes it in the chat as *Auto-allowed*. With **Allow everything**, or after **Always allow**,
|
||||
the agent's other questions are answered the same way and noted too.
|
||||
|
||||
## What it can't do
|
||||
|
||||
- **Files wait for you** unless you chose **Allow everything**. **Cancel** or **Stop** declines
|
||||
everything that's waiting, and a question nobody answers is declined after four minutes. The
|
||||
[MCP server's folder limits](mcp.md) still apply after you allow: the design's folder, the
|
||||
recovery folder and the server's own scratch folder.
|
||||
- **A limit per message.** The agent may make 24 changes per message by default; you can set
|
||||
the number on the AI page. Reading the design doesn't count. When it reaches the limit, it's
|
||||
told to stop and report what's left; send another message to let it carry on.
|
||||
- **No crash reports,** whatever the permissions. Crash reports can hold other designs and file
|
||||
paths.
|
||||
- **Not the rest of the app.** It can't click the interface, change settings, quit, or drive the
|
||||
chat itself.
|
||||
- **Its own tools stay off where we can turn them off.** SolidCraft declares no file system and
|
||||
no terminal to the agent, runs it in an empty working folder of its own, and asks the Claude
|
||||
Code adapter to turn off its shell, file and web tools for the session.
|
||||
|
||||
When the panel's agent is stopped, another agent is picked, or SolidCraft quits, the agent and
|
||||
every program it started (the CLI it wraps, its MCP server) are stopped.
|
||||
|
||||
## Privacy
|
||||
|
||||
Nothing goes anywhere except to the agent you run, or the endpoint you added. SolidCraft starts
|
||||
a coding agent as a program on your computer and talks to it over its standard input and
|
||||
output. What the agent sends to its own service, under your account and that service's terms,
|
||||
is between you and that service, exactly as when you use the agent in a terminal. SolidCraft
|
||||
never sees, stores or passes on a coding agent's password, token or key. An endpoint's key is
|
||||
kept in the system keychain and sent only to that endpoint. SolidCraft's own connection to the
|
||||
agent's tools uses a random per-launch token in a file only you can read, which never passes
|
||||
through the agent.
|
||||
|
||||
## Trying it without an account
|
||||
|
||||
A scripted test agent is in the source tree. It makes a block, rounds it and measures it, with
|
||||
no model behind it:
|
||||
|
||||
```sh
|
||||
cargo build --release -p solidcraft-ui-egui --example acp_test_agent
|
||||
```
|
||||
|
||||
Then add it under Preferences → AI → **Add agent… → Custom command** with the path of
|
||||
`target/release/examples/acp_test_agent`. Put "save" in your message to see an Allow / Deny card,
|
||||
or "ask" to see the agent ask its own permission.
|
||||
@@ -17,6 +17,7 @@
|
||||
- [Updates and privacy](guide/updates.md)
|
||||
- [Troubleshooting: SolidCraft won't start](guide/troubleshooting.md)
|
||||
- [Driving SolidCraft with an AI agent (MCP)](guide/mcp.md)
|
||||
- [Agent chat](guide/agent-chat.md)
|
||||
|
||||
# Developer guide
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{{#include ../../../agent-chat.md}}
|
||||
@@ -132,10 +132,11 @@ unit regardless of the design, pass an expression with one: `"value": "50 mm"` o
|
||||
| `ui.home` | `open?`: bool; `sample?`: index | the start page: show or hide it, open a built-in sample (once built); returns `open` and the recent designs |
|
||||
| `ui.shortcut` | `command`, `key?`, `replace?` | a command's key, or rebinds it (`""` clears; a key another command uses is refused unless `replace`) |
|
||||
| `ui.help` | `item?`: `about`, `shortcuts` (read only), `report` (opens a report draft for review), `crashes` (the Crash Reports window) | the version, commit, build date and diagnostic text |
|
||||
| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `blender`, `tinkercad`, `openscad`, `touchpad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, except Touchpad always zooms; pinch zooms), `orbit_around` (`view_centre` (default: what Pan moves and Fit frames), `origin` (the world origin stays still on screen), `cursor` (the model point under the pointer when the drag began); the older `orbit_cursor` boolean still sets `cursor` when true), `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`, `de`, `fr`, `ko`, `ru`, `uk`, `zh`; see [localization](localization.md)), `ui_scale`, `up_axis` (`z`, `y`), `offer_stand_up`, `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778), `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` |
|
||||
| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `blender`, `tinkercad`, `openscad`, `touchpad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, except Touchpad always zooms; pinch zooms), `orbit_around` (`view_centre` (default: what Pan moves and Fit frames), `origin` (the world origin stays still on screen), `cursor` (the model point under the pointer when the drag began); the older `orbit_cursor` boolean still sets `cursor` when true), `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`, `de`, `fr`, `ko`, `ru`, `uk`, `zh`; see [localization](localization.md)), `ui_scale`, `up_axis` (`z`, `y`), `offer_stand_up`, `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778), `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection, AI) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` |
|
||||
| `ui.capture` | `action?`: `capture`, `revert`, `cancel` | answers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without `action`, what waits |
|
||||
| `ui.window` | `action`: minimize, maximize, restore, toggle, close | what the title bar's caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first |
|
||||
| `ui.agent_cursor` | `show` (bool), `speed`: a number from 1 to 5 (times the normal speed) or `"instant"` (no animation: the command runs at once and the pointer jumps to where it acted), `follow_camera` (bool); all optional | the settings. While the cursor is shown, `engine.execute` first glides to the command's toolbar button and to each pick, then runs and replies; later requests wait their turn. `"animate": false` on a call skips it |
|
||||
| `ui.agent_chat` | `features?`, `toolbar_button?`, `permissions?` (`ask`, `edits`, `all`), `add_agent?` ({`name`, `kind`: `acp` or `api`, `command` or `base_url` + `model`}; no keys), `default_agent?`, `agent?` (the conversation's agent, by id), `mode?` (the agent's own mode), `open?`, `side?` (`left`, `right`), `minimized?`, `width?`, `picker?`, `refresh?`, `connect?`, `stop?`, `send?`: text, `attach_view?`, `permission?`: an option id or `"cancel"` (the newest question of the agent's own), `approve?`: true, false or `"always"` (the oldest request held for Allow/Deny), `cancel?`, `undo?` (the last message's changes) | the chat's state: `features`, `agents` (found and added), `currentAgent`, `state` (off, starting, ready, busy, auth, exited), `agent`, `modes`, `mode`, `authMethods`, `toolsPort`, `pid`, `waiting`, `turn`, `detection` and the conversation (`items`). With `features` off only the settings apply. Desktop only; see [Agent chat](agent-chat.md) |
|
||||
| `ui.resize` | `width`, `height` | |
|
||||
| `app.quit` | `force?` (quit without asking about unsaved designs) | `quitting` (the window closes), `asking` (a "save changes?" prompt is open) |
|
||||
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
# In-app agent chat over ACP
|
||||
|
||||
Status: desktop only, behind Preferences → AI → *Show AI features* (on by default; off hides and
|
||||
stops everything). The user guide is
|
||||
[docs/agent-chat.md](../agent-chat.md). This note records what I found about the Agent Client
|
||||
Protocol and the agents that speak it, how the chat is built, and what's still open.
|
||||
|
||||
## Goal
|
||||
|
||||
A chat panel inside SolidCraft that drives a coding agent **the user already has and is already
|
||||
signed in to** (Claude Code, Codex, Gemini CLI, …). SolidCraft holds no API keys and no
|
||||
accounts: the agent runs as a child process under the user's own login, and SolidCraft gives
|
||||
it one thing, its MCP server bridged to the open window, so the agent models on the design the
|
||||
user is looking at.
|
||||
|
||||
## The protocol
|
||||
|
||||
[ACP](https://agentclientprotocol.com) is JSON-RPC 2.0 over the agent's stdin/stdout, one
|
||||
message per line, protocol version `1`. The editor (here SolidCraft) is the *client*; the agent
|
||||
is the *server*, started as a subprocess. Zed introduced it to embed Claude Code and Codex;
|
||||
the Rust SDK (`agent-client-protocol`, Apache-2.0) and the schema crate
|
||||
(`agent-client-protocol-schema`, Apache-2.0) are the reference.
|
||||
|
||||
What the prototype uses:
|
||||
|
||||
| Direction | Message | Use |
|
||||
|---|---|---|
|
||||
| → agent | `initialize {protocolVersion: 1, clientCapabilities, clientInfo}` | We declare no file system and no terminal (`fs.readTextFile/writeTextFile: false`, `terminal: false`), and terminal login (`auth.terminal: true`, plus the older `_meta["terminal-auth"]`). The reply carries `agentInfo` and `authMethods`. |
|
||||
| → agent | `session/new {cwd, mcpServers}` | `mcpServers` holds one stdio server: `solidcraft-cli mcp --connect 127.0.0.1:PORT`. `cwd` is the open design's folder (else SolidCraft's own `agent` folder). |
|
||||
| → agent | `session/prompt {sessionId, prompt: [content]}` | One text block. The reply comes at the end of the turn with `stopReason` (`end_turn`, `cancelled`, `refusal`, …). |
|
||||
| → agent | `session/cancel {sessionId}` (notification) | Cancel button. Open permission prompts are answered `cancelled` first, as the spec asks. |
|
||||
| → agent | `authenticate {methodId}` | Only for an "agent" login method the user clicks (the agent then opens the browser itself). |
|
||||
| ← agent | `session/update` | `agent_message_chunk`, `agent_thought_chunk` (streamed text), `tool_call` / `tool_call_update` (`toolCallId`, `title`, `kind`, `status`: pending, in_progress, completed, failed, `content`), `plan` (entries with status). Mode, command and usage updates are ignored for now. |
|
||||
| ← agent | `session/request_permission {toolCall, options: [{optionId, name, kind}]}` | Shown as a prompt in the conversation; the answer is `{outcome: {outcome: "selected", optionId}}` or `{outcome: "cancelled"}`. `kind` is `allow_once`, `allow_always`, `reject_once` or `reject_always`. |
|
||||
| ← agent | `fs/*`, `terminal/*` | Not offered, answered "method not found". |
|
||||
| ← agent | `_auth/status_update` | An extension both current adapters send ("Not logged in"); shown next to the agent's name. |
|
||||
|
||||
Errors: an agent that is not signed in answers `session/new` (Codex) or the first
|
||||
`session/prompt` (Claude) with JSON-RPC error `-32000 "Authentication required"`.
|
||||
|
||||
## The agents
|
||||
|
||||
All checked on 2026-10-14 with the packages installed into a user prefix
|
||||
(`npm i -g --prefix ~/…`). Licences are from each package's own metadata; nothing is bundled
|
||||
with SolidCraft, the user installs the agent.
|
||||
|
||||
| Agent | How it speaks ACP | Package / licence | Version probe | Login |
|
||||
|---|---|---|---|---|
|
||||
| Claude Code | `claude-agent-acp` adapter (renamed from `@zed-industries/claude-code-acp`, which is now deprecated) | `@agentclientprotocol/claude-agent-acp` 0.90.0, Apache-2.0. It depends on `@anthropic-ai/claude-agent-sdk`, which is under Anthropic's own terms ("SEE LICENSE IN README.md"): fine for the user to install, not something we would vendor. | `--version` prints `0.90.0` | Terminal method. Offered only when the client declares terminal login. On a desktop: "Claude Subscription" = `claude-agent-acp --cli auth login --claudeai`, "Anthropic Console" = `… --cli auth login --console`. Over SSH (no browser) it offers `claude-login` = `… --cli` (the TUI `/login`). The login is Claude Code's own: `~/.claude/.credentials.json` (Linux, Windows) or the macOS keychain. |
|
||||
| Codex | `codex-acp` adapter (`@zed-industries/codex-acp` 0.16 moved to `@agentclientprotocol/codex-acp`) | `@agentclientprotocol/codex-acp` 2.2.2, Apache-2.0 | `--version` (0.16 rejected the flag; 2.x prints it) | Agent method `chat-gpt` (the adapter opens the browser for the ChatGPT login when the client calls `authenticate`), and `api-key` for a key the user configures in Codex. 0.16 also listed `env_var` methods for `CODEX_API_KEY` / `OPENAI_API_KEY`. The login is Codex's own: `~/.codex/auth.json`. |
|
||||
| Gemini CLI | built in: `gemini --acp` (the ACP registry's current flag; older releases used `--experimental-acp`) | `@google/gemini-cli` 0.64, Apache-2.0 | `--version` | Its own Google login (`~/.gemini/oauth_creds.json`) or a key in the environment. |
|
||||
| OpenCode | built in: `opencode acp` | `opencode-ai` 1.19, MIT | `--version` | Its own providers' logins (`~/.local/share/opencode/auth.json`) or keys in the environment. |
|
||||
| Others in the ACP registry | `copilot --acp`, `qwen --acp`, `opencode acp`, `goose acp`, `cline --acp`, `kimi acp`, … | mixed; several are proprietary, one is GPL and one AGPL (we only start them as programs, never link them) | `--version` | each its own |
|
||||
|
||||
The registry (`https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json`) lists
|
||||
about forty agents with their `npx`/binary distribution; a later version could read it to fill
|
||||
the picker instead of the hard-coded table.
|
||||
|
||||
### The auth story
|
||||
|
||||
- **SolidCraft never sees a credential.** It doesn't pass API keys, doesn't store tokens and
|
||||
doesn't open login pages by itself. Each agent keeps its own login where its CLI always does
|
||||
(Claude Code: `~/.claude` or the keychain; Codex: `~/.codex`; Gemini: `~/.gemini`). A user
|
||||
who already uses the agent in a terminal is already signed in, and the chat simply works.
|
||||
- **Not signed in:** the agent answers "Authentication required". The panel switches to
|
||||
*Sign-in needed* and lists the agent's own `authMethods`:
|
||||
- *terminal* methods are shown as the exact command to run in a terminal (with Copy), e.g.
|
||||
`claude-agent-acp --cli auth login --claudeai`; the user runs it, then presses Connect;
|
||||
- *agent* methods (Codex's ChatGPT login) get a button that calls `authenticate`; the agent
|
||||
opens the browser itself;
|
||||
- *env_var* methods say which variable to set in the user's environment. The panel has no
|
||||
field for a key, on purpose.
|
||||
- Verified here, against both adapters, with an empty home folder (no credentials, so nobody's
|
||||
account was used): Claude's adapter returns `-32000 Authentication required` on the first
|
||||
prompt and offers the terminal login methods; Codex's returns it on `session/new` and offers
|
||||
`chat-gpt` and `api-key`. I stopped at that step.
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
SolidCraft window ──(child process, own process group, ACP over stdio)──> agent (claude-agent-acp, codex-acp, gemini --acp, opencode acp, …)
|
||||
▲ │ starts, per session/new
|
||||
│ the chat's own endpoint (loopback, fresh token, gate) ▼
|
||||
└──────────────────────────────────────────────────── solidcraft-cli mcp --connect 127.0.0.1:PORT
|
||||
```
|
||||
|
||||
Code: `crates/ui-egui/src/agent_chat/`.
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `backend.rs` | The `ChatBackend` trait and its `Event`s: the only thing the panel talks to. |
|
||||
| `acp.rs`, `acp_client.rs` | The protocol messages, and `AcpBackend`, the first backend. |
|
||||
| `api_client.rs` | `ApiBackend`: an OpenAI-compatible endpoint, with SolidCraft running the tool loop. |
|
||||
| `agents.rs` | The agents (found and added), permission modes, "always allow" answers. |
|
||||
| `settings.rs` | Preferences → AI and the Add agent dialog. |
|
||||
| `process.rs` | The agent as a child process in a process group of its own; stopping it ends the group. |
|
||||
| `discover.rs` | Which agents are installed, their versions, whether they look signed in. |
|
||||
| `gate.rs` | What the agent's tool requests may do: run, wait for Allow/Deny, or never. |
|
||||
| `mod.rs` | The conversation, turns (undo as one step, the per-turn limit), held requests. |
|
||||
| `panel.rs` | The panel: docking, minimising, picker, sign-in, conversation, input. |
|
||||
|
||||
### A pluggable backend
|
||||
|
||||
The panel doesn't know which protocol it's talking. A backend implements `ChatBackend` (`poll`
|
||||
for events, `prompt`, `cancel`, `answer_permission`, `authenticate`, `set_mode`, `shutdown`) and
|
||||
gets a `StartContext` with its working folder, the MCP bridge and `tools` (a sender for control
|
||||
requests, for a backend that runs tools in process). Two backends exist:
|
||||
|
||||
- `AcpBackend` drives a coding agent the user already has (the default, and the point of the
|
||||
feature: the user's own account, no keys).
|
||||
- `ApiBackend` talks to an OpenAI-compatible chat completions endpoint (Ollama, LM Studio,
|
||||
OpenAI, a custom URL) and runs the tool loop itself: it starts the same `solidcraft-cli mcp
|
||||
--connect` server an ACP agent would get and is its MCP client (`tools/list` becomes the
|
||||
function tools, `crash_reports` left out), so its calls pass the same gate, limits, questions
|
||||
and undo as an ACP agent's. At most 24 model requests per message. The HTTP request is the
|
||||
desktop host's (`Services::agent_http`, ureq with no redirects); the key comes from the system
|
||||
keychain (`Services::secrets`, the keyring crate) for the connection only and is refused over
|
||||
plain HTTP to another computer.
|
||||
|
||||
Another kind of backend slots in the same way without reshaping the panel.
|
||||
|
||||
### Design tools and the gate
|
||||
|
||||
On the first Connect the app starts **an endpoint of the chat's own**
|
||||
(`control_server::listen_agent`): a free loopback port with a fresh random token, written to the
|
||||
per-user token file where `solidcraft-cli mcp --connect` finds it. The token never passes
|
||||
through the agent, `SOLIDCRAFT_CONTROL_TOKEN` is removed from the agent's environment, and
|
||||
`--allow-any-path` is never passed, so the MCP server keeps its #205 folder limits (a request
|
||||
that names no folders is confined to the chat's working folder).
|
||||
|
||||
Requests from that endpoint don't go to the app directly. `pump_tools` passes each one through
|
||||
`gate::request`:
|
||||
|
||||
- **Methods:** only `engine.commands`, `document.inspect`, `engine.execute`, `engine.batch`,
|
||||
`ui.view`, `ui.render` and `ui.screenshot` (what the MCP server uses). No clicks, preferences,
|
||||
quitting, scripts or the chat itself.
|
||||
- **Refused:** `diagnostics.*` (crash reports hold other designs and paths), `app.*`.
|
||||
- **Allow/Deny in the panel:** `file.*`, `doc.*`, render export, any command given a file or
|
||||
folder, a picture saved outside the MCP server's scratch folder, and `edit.undo` past the
|
||||
turn's first change. The request waits (the endpoint is patient) until the user answers;
|
||||
Cancel/Stop declines everything waiting, and after four minutes it is declined (the MCP
|
||||
server stops waiting at five).
|
||||
- **Permission modes** (Preferences → AI, global with a per-agent override): *Ask every time*
|
||||
turns every change into a question too; *Auto-allow model edits* (default) is the list above;
|
||||
*Allow everything* runs what would ask (never what is refused). "Always allow" on a card
|
||||
remembers the command per agent. The agent's own ACP permission questions about SolidCraft's
|
||||
tools are answered "allow once" by the client (the gate decides), and others follow the mode
|
||||
and the remembered answers; every automatic answer leaves an *Auto-allowed* note.
|
||||
- **Per-turn limit:** 24 requests that run commands (a batch counts once; reads don't count),
|
||||
set in Preferences, and 400 requests of any kind. Past it the agent is told to stop and say
|
||||
what's left.
|
||||
- **Outside a turn:** refused.
|
||||
|
||||
What runs goes to the same queue as the control channel's held requests, so file loads, the
|
||||
agent cursor and the per-request folder scope all apply as for any other client.
|
||||
|
||||
### One undo step per message
|
||||
|
||||
When a message is sent the chat remembers the design (`Arc<Document>`) and the undo depth. When
|
||||
the turn ends, the undo snapshots made since are merged into the first one (which holds the
|
||||
design from before the turn), relabelled "Agent chat: <message>". If the history was trimmed in
|
||||
the meantime the start is found by the design it holds; if it's gone, nothing is merged. The
|
||||
conversation shows the merged step with an Undo button while it's still the latest thing done.
|
||||
|
||||
### The agent's own tools
|
||||
|
||||
ACP lets the client offer a file system and a terminal; the chat offers neither, and starts the
|
||||
agent in an empty working folder of SolidCraft's own (`<data>/agent-chat/work`), not the
|
||||
design's folder. The Claude Code adapter has shell, file and web tools of its own anyway: the
|
||||
session asks it to turn them off (`_meta.claudeCode.options.disallowedTools`) and not to offer
|
||||
"bypass permissions". Other agents' own permission requests appear in the panel with their
|
||||
options.
|
||||
|
||||
### Processes
|
||||
|
||||
The agent starts in a process group of its own (Unix; on Windows `taskkill /T` ends its tree).
|
||||
Stop, Close-and-Stop, turning the preference off, the agent chat being dropped and quitting all
|
||||
end it the same way: its stdin closes (ACP agents end at end of input), then after a grace period
|
||||
the group gets TERM, and KILL after another. Quitting waits for this (at most about three
|
||||
seconds). `process.rs`'s tests start a fake agent that leaves helpers behind, once polite and
|
||||
once ignoring both end of input and TERM, and check that none survive. If SolidCraft itself is
|
||||
killed, the agent sees its stdin close and ends with its helpers.
|
||||
|
||||
### Discovery
|
||||
|
||||
As before, plus key variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, …)
|
||||
reported as *key set*, `CLAUDE_CONFIG_DIR` and `CODEX_HOME` respected, and OpenCode's
|
||||
`~/.local/share/opencode/auth.json`. Nothing is started except `--version`.
|
||||
|
||||
### Panel
|
||||
|
||||
Opened from the **AI** button at the toolbar row's right end (the panels give it room and spill
|
||||
into » as usual; Preferences → AI → *Show button in toolbar* hides it), Ctrl+Shift+A and the View
|
||||
settings menu. Docked left or right (dragging the header across the window, or Preferences), resizable, folded
|
||||
to a 30 px strip with Minimise (state dot and a count of open questions), closed with ×. Side,
|
||||
width, fold and the per-turn limit are preferences, not design data. Ctrl+Shift+A is reserved in the shortcut editor and works from the chat's text box too. The
|
||||
header has the agent switcher (per conversation) and, for agents with ACP `modes`, the agent's
|
||||
own mode picker (`session/set_mode`).
|
||||
|
||||
### Control channel
|
||||
|
||||
`ui.agent_chat` (docs/control-protocol.md) sets the preferences, opens and docks the panel,
|
||||
connects, sends, answers permission and approval prompts, cancels, undoes the last message and
|
||||
returns the whole state, for tests and screenshots.
|
||||
|
||||
## Still open
|
||||
|
||||
| Area | Work |
|
||||
|---|---|
|
||||
| Conversation | Markdown rendering, diffs, history per design, resume (`session/load`) |
|
||||
| Session controls | Model and mode pickers from `configOptions` / `modes`, slash commands |
|
||||
| Auth | An embedded terminal or "open a terminal here" for terminal logins on each OS; logout |
|
||||
| Discovery | Read the ACP registry; keychain-aware login check on macOS |
|
||||
| Packaging | Make sure every desktop package ships `solidcraft-cli` next to `solidcraft` (Flatpak, AppImage, macOS bundle, Windows zip), or serve MCP from the app binary itself |
|
||||
| Windows | Job objects instead of `taskkill /T` once there's a safe API for them |
|
||||
| API backend | Streaming answers; images in tool results |
|
||||
Reference in new issue
Block a user