Thanks @Rec0iL.
This commit is contained in:
1 parent
920ea42fe1
commit
04b4381b54
5 files changed
+101
-14
No files matched your search
@@ -1,5 +1,5 @@
|
||||
//! The agents the chat can use: the installed ones it finds by itself (Claude Code, Codex,
|
||||
//! Gemini CLI, OpenCode, …) and the ones the user adds (another ACP command, or an
|
||||
//! Antigravity, OpenCode, …) and the ones the user adds (another ACP command, or an
|
||||
//! OpenAI-compatible endpoint such as Ollama, LM Studio or OpenAI). Also the permission modes.
|
||||
//! Preferences only: no design data, no keys (those are in the system keychain).
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ pub struct Known {
|
||||
pub config_dir_var: Option<(&'static str, &'static str)>,
|
||||
/// Variables holding a key it uses instead of a login.
|
||||
pub key_vars: &'static [&'static str],
|
||||
/// Retired by its maker: still used when it is installed, never offered for install.
|
||||
pub retired: bool,
|
||||
/// Whether `--version` prints its version. Antigravity's server prints its build date and takes
|
||||
/// seconds to start, so it is not asked.
|
||||
pub reports_version: bool,
|
||||
}
|
||||
|
||||
/// The agents shown in the picker. The first one found is the default.
|
||||
@@ -37,6 +42,8 @@ pub const KNOWN: &[Known] = &[
|
||||
credentials: Some(".claude/.credentials.json"),
|
||||
config_dir_var: Some(("CLAUDE_CONFIG_DIR", ".credentials.json")),
|
||||
key_vars: &["ANTHROPIC_API_KEY"],
|
||||
retired: false,
|
||||
reports_version: true,
|
||||
},
|
||||
Known {
|
||||
id: "codex",
|
||||
@@ -47,7 +54,25 @@ pub const KNOWN: &[Known] = &[
|
||||
credentials: Some(".codex/auth.json"),
|
||||
config_dir_var: Some(("CODEX_HOME", "auth.json")),
|
||||
key_vars: &["OPENAI_API_KEY", "CODEX_API_KEY"],
|
||||
retired: false,
|
||||
reports_version: true,
|
||||
},
|
||||
// Google's `agy` command line has no ACP mode; its ACP server is a separate download that the
|
||||
// ACP registry lists (`agy_acp_server.par`, `agy_acp_server.exe` on Windows).
|
||||
Known {
|
||||
id: "antigravity",
|
||||
name: "Google Antigravity",
|
||||
programs: &["agy_acp_server.par", "agy_acp_server"],
|
||||
args: &[],
|
||||
install: "download the Antigravity ACP server for your system (see agentclientprotocol.com/registry), unzip it and put agy_acp_server.par on your PATH",
|
||||
credentials: Some(".gemini/antigravity-acp/acp_token.json"),
|
||||
config_dir_var: None,
|
||||
key_vars: &["GEMINI_API_KEY"],
|
||||
retired: false,
|
||||
reports_version: false,
|
||||
},
|
||||
// Google retired it on 18 June 2026 in favour of Antigravity (#1790). Enterprise licences may
|
||||
// still work, so an installed one is found, but nobody is sent to install it.
|
||||
Known {
|
||||
id: "gemini",
|
||||
name: "Gemini CLI",
|
||||
@@ -57,6 +82,8 @@ pub const KNOWN: &[Known] = &[
|
||||
credentials: Some(".gemini/oauth_creds.json"),
|
||||
config_dir_var: None,
|
||||
key_vars: &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
||||
retired: true,
|
||||
reports_version: true,
|
||||
},
|
||||
Known {
|
||||
id: "opencode",
|
||||
@@ -67,6 +94,8 @@ pub const KNOWN: &[Known] = &[
|
||||
credentials: Some(".local/share/opencode/auth.json"),
|
||||
config_dir_var: None,
|
||||
key_vars: &[],
|
||||
retired: false,
|
||||
reports_version: true,
|
||||
},
|
||||
Known {
|
||||
id: "grok",
|
||||
@@ -77,6 +106,8 @@ pub const KNOWN: &[Known] = &[
|
||||
credentials: Some(".grok/auth.json"),
|
||||
config_dir_var: None,
|
||||
key_vars: &["XAI_API_KEY"],
|
||||
retired: false,
|
||||
reports_version: true,
|
||||
},
|
||||
Known {
|
||||
id: "copilot",
|
||||
@@ -87,6 +118,8 @@ pub const KNOWN: &[Known] = &[
|
||||
credentials: None,
|
||||
config_dir_var: None,
|
||||
key_vars: &["GH_TOKEN", "GITHUB_TOKEN"],
|
||||
retired: false,
|
||||
reports_version: true,
|
||||
},
|
||||
Known {
|
||||
id: "qwen",
|
||||
@@ -97,6 +130,8 @@ pub const KNOWN: &[Known] = &[
|
||||
credentials: Some(".qwen/oauth_creds.json"),
|
||||
config_dir_var: None,
|
||||
key_vars: &["DASHSCOPE_API_KEY"],
|
||||
retired: false,
|
||||
reports_version: true,
|
||||
},
|
||||
Known {
|
||||
id: "goose",
|
||||
@@ -107,6 +142,8 @@ pub const KNOWN: &[Known] = &[
|
||||
credentials: None,
|
||||
config_dir_var: None,
|
||||
key_vars: &[],
|
||||
retired: false,
|
||||
reports_version: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -292,19 +329,24 @@ fn login_of(k: &Known, home: Option<&Path>) -> Login {
|
||||
|
||||
/// Look for every known agent. Blocking (each probe has a time limit): run it off the UI thread.
|
||||
pub fn detect() -> Detection {
|
||||
detect_in(&search_dirs(), home().as_deref())
|
||||
}
|
||||
|
||||
fn detect_in(dirs: &[PathBuf], home: Option<&Path>) -> Detection {
|
||||
let t0 = Instant::now();
|
||||
let dirs = search_dirs();
|
||||
let home = home();
|
||||
let mut d = Detection::default();
|
||||
for k in KNOWN {
|
||||
let Some(path) = k.programs.iter().find_map(|p| find_in(p, &dirs)) else {
|
||||
d.missing.push(Missing { id: k.id, name: k.name, install: k.install });
|
||||
let Some(path) = k.programs.iter().find_map(|p| find_in(p, dirs)) else {
|
||||
if !k.retired {
|
||||
d.missing.push(Missing { id: k.id, name: k.name, install: k.install });
|
||||
}
|
||||
continue;
|
||||
};
|
||||
let login = login_of(k, home.as_deref());
|
||||
let login = login_of(k, home);
|
||||
let mut argv = vec![path.to_string_lossy().into_owned()];
|
||||
argv.extend(k.args.iter().map(|a| a.to_string()));
|
||||
d.found.push(Found { id: k.id, name: k.name, version: version(&path, Duration::from_secs(3)), argv, login });
|
||||
let version = if k.reports_version { version(&path, Duration::from_secs(3)) } else { None };
|
||||
d.found.push(Found { id: k.id, name: k.name, version, argv, login });
|
||||
}
|
||||
d.took_ms = u64::try_from(t0.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
d
|
||||
@@ -376,6 +418,14 @@ mod tests {
|
||||
if std::env::var_os("XAI_API_KEY").is_none() {
|
||||
assert_eq!(login_of(grok, Some(&home)), Login::NotLoggedIn);
|
||||
}
|
||||
// Antigravity's server keeps its own login, apart from the retired Gemini CLI's.
|
||||
let agy = KNOWN.iter().find(|k| k.id == "antigravity").unwrap();
|
||||
if std::env::var_os("GEMINI_API_KEY").is_none() {
|
||||
assert_eq!(login_of(agy, Some(&home)), Login::NotLoggedIn, "the Gemini CLI's file is not its login");
|
||||
}
|
||||
std::fs::create_dir_all(home.join(".gemini/antigravity-acp")).unwrap();
|
||||
std::fs::write(home.join(".gemini/antigravity-acp/acp_token.json"), "not credential data").unwrap();
|
||||
assert_eq!(login_of(agy, Some(&home)), Login::LooksLoggedIn);
|
||||
std::fs::create_dir_all(home.join(".grok")).unwrap();
|
||||
// Discovery checks only for the file; it never needs to parse the user's tokens.
|
||||
std::fs::write(home.join(".grok/auth.json"), "not credential data").unwrap();
|
||||
@@ -383,6 +433,42 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(home);
|
||||
}
|
||||
|
||||
/// Google retired the Gemini CLI (#1790): one that is installed is still found, but nobody is
|
||||
/// told to install it. Antigravity's ACP server is offered, and found under its file name.
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn a_retired_agent_is_found_but_not_offered_and_antigravity_is() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let root = std::env::temp_dir().join(format!("sc-agent-retired-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let run = |name: &str, body: &str| {
|
||||
let path = root.join(name);
|
||||
std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap();
|
||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
};
|
||||
let ids = |d: &Detection| (d.found.iter().map(|f| f.id).collect::<Vec<_>>(), d.missing.iter().map(|m| m.id).collect::<Vec<_>>());
|
||||
|
||||
let none = detect_in(std::slice::from_ref(&root), None);
|
||||
let (found, missing) = ids(&none);
|
||||
assert!(found.is_empty());
|
||||
assert!(missing.contains(&"antigravity") && !missing.contains(&"gemini"), "{missing:?}");
|
||||
let hint = none.missing.iter().find(|m| m.id == "antigravity").map(|m| m.install).unwrap_or_default();
|
||||
assert!(hint.contains("agy_acp_server.par") && !hint.contains("npm"), "{hint}");
|
||||
|
||||
run("gemini", "echo gemini 0.64.0");
|
||||
// A server that would take seconds to answer `--version`, which Antigravity is never asked.
|
||||
run("agy_acp_server.par", "sleep 30");
|
||||
let both = detect_in(std::slice::from_ref(&root), None);
|
||||
let (found, missing) = ids(&both);
|
||||
assert_eq!(found, ["antigravity", "gemini"]);
|
||||
assert!(!missing.contains(&"antigravity") && !missing.contains(&"gemini"), "{missing:?}");
|
||||
let agy = both.found.iter().find(|f| f.id == "antigravity");
|
||||
assert_eq!(agy.map(|f| f.version.clone()), Some(None), "its --version is a build date, not asked");
|
||||
assert_eq!(agy.and_then(|f| f.argv.get(1)), None, "the registry starts it without arguments");
|
||||
assert!(both.took_ms < 5000, "Antigravity's server was started to read a version");
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_known_agent_has_a_program_and_an_install_hint() {
|
||||
for k in KNOWN {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Agent chat: a side panel that talks to a coding agent the user already has (Claude Code,
|
||||
//! Codex, Gemini CLI, OpenCode, …) over the Agent Client Protocol, and gives that agent
|
||||
//! Codex, Antigravity, OpenCode, …) over the Agent Client Protocol, and gives that agent
|
||||
//! SolidCraft's MCP server, bridged to this window, as its way to read and change the open design
|
||||
//! (docs/agent-chat.md, docs/design/in-app-agent-chat.md).
|
||||
//!
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
# Agent chat
|
||||
|
||||
Agent chat is a side panel where you talk to a coding agent you already use (Claude Code,
|
||||
Codex, Gemini CLI, OpenCode, Grok Build and others), or to a model behind an OpenAI-compatible endpoint you
|
||||
Codex, Google Antigravity, OpenCode, Grok Build 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
|
||||
@@ -88,7 +88,7 @@ the keychain, so SolidCraft doesn't guess there.
|
||||
|---|---|
|
||||
| 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`) |
|
||||
| Google Antigravity | Google's ACP server, a separate download from Google's `agy` command (which has no ACP mode): take the zip for your system from the [ACP registry](https://agentclientprotocol.com/registry), unzip it and put `agy_acp_server.par` (`agy_acp_server.exe` on Windows) on your `PATH`. The zip also holds `localharness_external`: leave the two together. It signs in itself (browser). |
|
||||
| OpenCode | `npm install -g opencode-ai` (`opencode acp`) |
|
||||
| XAI Grok | [Grok Build installer](https://docs.x.ai/build/overview), then `grok login` (`grok agent stdio` speaks ACP) |
|
||||
| GitHub Copilot CLI, Qwen Code, goose | listed with their install command when missing |
|
||||
|
||||
@@ -8,7 +8,7 @@ Protocol and the agents that speak it, how the chat is built, and what's still o
|
||||
## 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
|
||||
signed in to** (Claude Code, Codex, Antigravity, …). 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.
|
||||
@@ -48,7 +48,8 @@ with SolidCraft, the user installs the agent.
|
||||
|---|---|---|---|---|
|
||||
| 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. |
|
||||
| Google Antigravity | a separate server, `agy_acp_server.par` (`.exe` on Windows), from the ACP registry's zip (`agy-acp-server-1.4.0-…`, with `localharness_external` beside it, ~330 MB). The `agy` command (1.4.0) rejects `--acp`; its `--input-format stream-json` print mode is not ACP. Started without arguments (the registry's Linux entry passes an empty `--uid=`, not needed here). | proprietary (Google's terms) | none: `--version` prints a build date and takes about two seconds, so it is not asked | Agent methods `oauth-personal`, `oauth-business`, `gemini-api-key` (`GEMINI_API_KEY`), `agent-platform`; `session/new` answers "Authentication required" until one is chosen. Settings in `~/.gemini/antigravity-acp/settings.json`. |
|
||||
| Gemini CLI (retired) | `gemini --acp` | `@google/gemini-cli` 0.64, Apache-2.0 | `--version` | Google ended it for consumer accounts on 2026-06-18 in favour of Antigravity. Still found when installed (enterprise licences may keep it working) but no longer offered for install. Its own 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. |
|
||||
| XAI Grok | built in: `grok agent stdio` | [Grok Build](https://github.com/xai-org/grok-build), Apache-2.0; installed separately | `grok --version` prints `grok 1.0.58 (…) [alpha]` (checked 2026-10-15; in-app connection also checked with 1.0.60) | Its own `~/.grok/auth.json` or `XAI_API_KEY`. `grok login` signs in in the browser; ACP advertises `cached_token` and `grok.com` authentication methods. |
|
||||
| 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 |
|
||||
@@ -61,7 +62,7 @@ the picker instead of the hard-coded table.
|
||||
|
||||
- **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
|
||||
(Claude Code: `~/.claude` or the keychain; Codex: `~/.codex`; Gemini and Antigravity: `~/.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`:
|
||||
@@ -79,7 +80,7 @@ the picker instead of the hard-coded table.
|
||||
## How it works
|
||||
|
||||
```
|
||||
SolidCraft window ──(child process, own process group, ACP over stdio)──> agent (claude-agent-acp, codex-acp, gemini --acp, opencode acp, …)
|
||||
SolidCraft window ──(child process, own process group, ACP over stdio)──> agent (claude-agent-acp, codex-acp, agy_acp_server, opencode acp, …)
|
||||
▲ │ starts, per session/new
|
||||
│ the chat's own endpoint (loopback, fresh token, gate) ▼
|
||||
└──────────────────────────────────────────────────── solidcraft-cli mcp --connect 127.0.0.1:PORT
|
||||
|
||||
Reference in new issue
Block a user