Skip to content

Commit 1b296f4

Browse files
committed
feat(desktop): add Gemini CLI as a tier-2 preset harness
Gemini CLI ships native ACP support (gemini --acp) with no adapter needed, but had no PresetHarness entry. Adds the preset (command, args, install hint/URL), an inline SVG mark, and settings copy. Splits the tier-2 preset block (struct, PRESET_HARNESSES, and its two accessor fns) out of discovery.rs into a new sibling module discovery/presets.rs — discovery.rs was already past the desktop file-size ratchet, so a bare addition to PRESET_HARNESSES would have failed just desktop-check. No behavior change from the split itself. Signed-off-by: JPeetz <88510961+JPeetz@users.noreply.github.com>
1 parent 485d03a commit 1b296f4

6 files changed

Lines changed: 264 additions & 220 deletions

File tree

desktop/public/harness-logos/CREDITS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Monochrome marks inlined as `currentColor` paths in
2525
|---|---|---|---|---|---|
2626
| Goose | [block/goose](https://github.com/block/goose) | `305849b71709b95b86ed9f11bd3bc939899c0aab` | Apache-2.0 © Block, Inc. | `documentation/static/img/goose.svg` | `fill="#101010"``currentColor`; dropped the redundant clipPath wrapper |
2727
| Cursor | [simple-icons](https://github.com/simple-icons/simple-icons) | `16.27.1` (slug `cursor`) | CC0-1.0 (path data); nominative use of the Cursor mark to identify Cursor's harness | `icons/cursor.svg` | `fill``currentColor` |
28+
| Gemini | [simple-icons](https://github.com/simple-icons/simple-icons) | `16.27.1` (slug `googlegemini`) | CC0-1.0 (path data); nominative use of the Gemini mark to identify Gemini CLI's harness | `icons/googlegemini.svg` | `fill``currentColor` |
2829

2930
Codex deliberately has **no** bundled mark: the OpenAI blossom was removed
3031
from simple-icons in v16 at the vendor's request, so we do not ship it —

desktop/src-tauri/src/managed_agents/discovery.rs

Lines changed: 6 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,224 +1436,13 @@ pub(crate) fn discover_acp_runtime_availability(runtime_id: &str) -> Option<AcpA
14361436
.map(|partial| partial.entry.availability)
14371437
}
14381438

1439-
// ── Tier-2 preset harnesses ────────────────────────────────────────────────
1440-
//
1441-
// Static data for well-known ACP harnesses that have bundled logos and
1442-
// verified command/args. PATH-probed at discovery time (Detected badge);
1443-
// not editable or deletable by users. Logos are bundled assets referenced
1444-
// by id in the frontend `RUNTIME_LOGOS` map.
1445-
1446-
struct PresetHarness {
1447-
id: &'static str,
1448-
label: &'static str,
1449-
command: &'static str,
1450-
args: &'static [&'static str],
1451-
install_instructions_url: &'static str,
1452-
install_hint: &'static str,
1453-
/// Vendor CLI the ACP command wraps, when the preset is an adapter
1454-
/// (e.g. Amp's `amp-acp` wraps the separately-installed `amp` CLI).
1455-
/// Consulted only when the adapter is absent, so `AdapterMissing`
1456-
/// replaces the misleading `NotInstalled` when the CLI is present but
1457-
/// the adapter is not. Deliberately NOT fed through the builtins'
1458-
/// full `classify_runtime` predicate: that would flip
1459-
/// adapter-present/CLI-absent from today's `Available` to `CliMissing`
1460-
/// (unselectable), and presets carry a single flat `install_hint`, so
1461-
/// the `CliMissing` copy would tell the user to install the adapter
1462-
/// they already have. `None` when the command IS the vendor CLI.
1463-
underlying_cli: Option<&'static str>,
1464-
}
1465-
1466-
/// Build the catalog entry for one preset harness through an injectable
1467-
/// resolver — the seam the preset loop consumes and tests bind.
1468-
///
1469-
/// Availability consumes only the adapter-missing arm of the builtin
1470-
/// predicate: adapter presence alone decides `Available` (exactly today's
1471-
/// behavior — an `amp-acp` without `amp` stays selectable), and
1472-
/// `underlying_cli` is consulted only when the adapter is absent, to
1473-
/// distinguish `AdapterMissing` (vendor CLI present) from `NotInstalled`
1474-
/// (neither found). See the `underlying_cli` field doc for why the full
1475-
/// `classify_runtime` predicate is deliberately not used here.
1476-
fn preset_catalog_entry(
1477-
def: &PresetHarness,
1478-
resolve: impl Fn(&str) -> Option<PathBuf>,
1479-
) -> AcpRuntimeCatalogEntry {
1480-
let (availability, command, binary_path) = match resolve(def.command) {
1481-
Some(path) => (
1482-
AcpAvailabilityStatus::Available,
1483-
Some(def.command.to_string()),
1484-
Some(path.display().to_string()),
1485-
),
1486-
None => {
1487-
let underlying_cli_found = def
1488-
.underlying_cli
1489-
.map(|cli| resolve(cli).is_some())
1490-
.unwrap_or(false);
1491-
if underlying_cli_found {
1492-
(AcpAvailabilityStatus::AdapterMissing, None, None)
1493-
} else {
1494-
(AcpAvailabilityStatus::NotInstalled, None, None)
1495-
}
1496-
}
1497-
};
1498-
let underlying_cli_path = def
1499-
.underlying_cli
1500-
.and_then(resolve)
1501-
.map(|p| p.display().to_string());
1502-
1503-
let default_args = normalize_agent_args(
1504-
def.command,
1505-
def.args.iter().map(|s| s.to_string()).collect(),
1506-
);
1507-
1508-
AcpRuntimeCatalogEntry {
1509-
id: def.id.to_string(),
1510-
label: def.label.to_string(),
1511-
// No remote URL — all preset icons are bundled assets.
1512-
avatar_url: String::new(),
1513-
availability,
1514-
command,
1515-
binary_path,
1516-
default_args,
1517-
mcp_command: None,
1518-
model_env_var: None,
1519-
provider_env_var: None,
1520-
thinking_env_var: None,
1521-
install_hint: def.install_hint.to_string(),
1522-
install_instructions_url: def.install_instructions_url.to_string(),
1523-
can_auto_install: false,
1524-
// Kept false even for adapter presets: presets carry one flat
1525-
// install_hint (the adapter's), so the requiresExternalCli
1526-
// "CLI is missing" wording would pair the wrong noun with it.
1527-
// The builtin path, with per-availability hints, is the only
1528-
// consumer of the true case.
1529-
requires_external_cli: false,
1530-
underlying_cli_path,
1531-
node_required: false,
1532-
auth_status: AuthStatus::NotApplicable,
1533-
login_hint: None,
1534-
source: HarnessSource::Preset,
1535-
// Preset entries have static, non-editable env; definition_env is empty.
1536-
definition_env: Default::default(),
1537-
}
1538-
}
1439+
// Tier-2 preset harnesses (static PRESET_HARNESSES catalog, bundled logos,
1440+
// no PATH-installer) live in `presets.rs` — split out to keep this file
1441+
// under the desktop file-size ratchet as the preset list grows.
1442+
mod presets;
15391443

1540-
const PRESET_HARNESSES: &[PresetHarness] = &[
1541-
PresetHarness {
1542-
id: "cursor",
1543-
label: "Cursor",
1544-
command: "cursor-agent",
1545-
args: &["acp"],
1546-
install_instructions_url: "https://cursor.com/downloads",
1547-
install_hint: "Buzz talks to Cursor through the cursor-agent CLI's ACP mode.",
1548-
underlying_cli: None,
1549-
},
1550-
PresetHarness {
1551-
id: "omp",
1552-
label: "Oh My Pi",
1553-
command: "omp",
1554-
args: &["acp"],
1555-
install_instructions_url: "https://github.com/can1357/oh-my-pi",
1556-
install_hint: "Buzz talks to Oh My Pi through its CLI's ACP mode (omp acp).",
1557-
underlying_cli: None,
1558-
},
1559-
PresetHarness {
1560-
id: "grok",
1561-
label: "Grok Build",
1562-
command: "grok",
1563-
args: &["agent", "--always-approve", "stdio"],
1564-
install_instructions_url: "https://build.x.ai/docs",
1565-
install_hint: "Buzz talks to Grok Build through its CLI's agent stdio mode.",
1566-
underlying_cli: None,
1567-
},
1568-
PresetHarness {
1569-
id: "opencode",
1570-
label: "OpenCode",
1571-
command: "opencode",
1572-
args: &["acp"],
1573-
install_instructions_url: "https://opencode.ai/docs",
1574-
install_hint: "Buzz talks to OpenCode through its CLI's ACP mode (opencode acp).",
1575-
underlying_cli: None,
1576-
},
1577-
PresetHarness {
1578-
id: "kimi",
1579-
label: "Kimi Code",
1580-
command: "kimi",
1581-
args: &["acp"],
1582-
install_instructions_url: "https://kimi.ai/download",
1583-
install_hint: "Buzz talks to Kimi Code through its CLI's ACP mode (kimi acp).",
1584-
underlying_cli: None,
1585-
},
1586-
PresetHarness {
1587-
id: "amp",
1588-
label: "Amp",
1589-
command: "amp-acp",
1590-
args: &[],
1591-
install_instructions_url: "https://github.com/tao12345666333/amp-acp",
1592-
install_hint: "Buzz talks to the Amp CLI through the amp-acp adapter. Follow the setup guide to install the adapter so the amp-acp command is on your PATH.",
1593-
underlying_cli: Some("amp"),
1594-
},
1595-
PresetHarness {
1596-
id: "hermes",
1597-
label: "Hermes Agent",
1598-
command: "hermes-acp",
1599-
args: &[],
1600-
install_instructions_url: "https://hermes-agent.nousresearch.com",
1601-
install_hint: "Buzz talks to Hermes Agent through its hermes-acp command.",
1602-
underlying_cli: None,
1603-
},
1604-
PresetHarness {
1605-
id: "openclaw",
1606-
label: "OpenClaw",
1607-
command: "openclaw",
1608-
args: &["acp"],
1609-
install_instructions_url: "https://docs.openclaw.ai/start/getting-started",
1610-
install_hint: "Buzz talks to OpenClaw through its ACP mode (openclaw acp), which relies on the OpenClaw Gateway daemon. Follow the setup guide to install both.\n\n\
1611-
⚠️ Execution-locus note: `openclaw acp` runs tools inside the \
1612-
OpenClaw Gateway daemon, not in the Desktop process. \
1613-
Desktop-injected BUZZ_* env vars are visible to the `openclaw` \
1614-
harness process itself, but do NOT automatically reach the \
1615-
Gateway's execution environment. If your tools or agent logic \
1616-
needs BUZZ_* credentials at execution time, set them on the \
1617-
Gateway's own environment separately.",
1618-
underlying_cli: None,
1619-
},
1620-
];
1621-
1622-
/// Return the static preset harness definitions as `HarnessDefinition` values.
1623-
///
1624-
/// Used by `warm_harness_registry_from_dir` to seed the loaded-harness registry
1625-
/// at startup before the frontend triggers a full discovery run.
1626-
pub(crate) fn preset_harness_definitions(
1627-
) -> Vec<crate::managed_agents::custom_harnesses::HarnessDefinition> {
1628-
PRESET_HARNESSES
1629-
.iter()
1630-
.map(
1631-
|p| crate::managed_agents::custom_harnesses::HarnessDefinition {
1632-
id: p.id.to_string(),
1633-
label: p.label.to_string(),
1634-
command: p.command.to_string(),
1635-
args: p.args.iter().map(|s| s.to_string()).collect(),
1636-
env: std::collections::BTreeMap::new(),
1637-
install_instructions_url: p.install_instructions_url.to_string(),
1638-
install_hint: p.install_hint.to_string(),
1639-
},
1640-
)
1641-
.collect()
1642-
}
1643-
1644-
/// Return the static slice of preset harness IDs.
1645-
///
1646-
/// Used by `check_id_collision` in `custom_harnesses` to derive the reserved-ID
1647-
/// set from the single source of truth (`PRESET_HARNESSES`) rather than a
1648-
/// hand-maintained copy. Adding a preset automatically reserves its ID.
1649-
pub(crate) fn preset_harness_ids() -> &'static [&'static str] {
1650-
// `PRESET_HARNESSES` is `'static`; we project its `id` fields.
1651-
// Computed once via OnceLock to avoid repeated allocations on hot paths.
1652-
use std::sync::OnceLock;
1653-
static IDS: OnceLock<Vec<&'static str>> = OnceLock::new();
1654-
IDS.get_or_init(|| PRESET_HARNESSES.iter().map(|p| p.id).collect())
1655-
.as_slice()
1656-
}
1444+
use presets::{preset_catalog_entry, PRESET_HARNESSES};
1445+
pub(crate) use presets::{preset_harness_definitions, preset_harness_ids};
16571446

16581447
/// Discover all ACP runtimes, optionally merging user-defined custom harnesses
16591448
/// from `custom_harnesses_dir`.

0 commit comments

Comments
 (0)