diff --git a/CLAUDE.md b/CLAUDE.md index e1ac653338..3dec141091 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Paseo is a mobile app for monitoring and controlling your local AI coding agents from anywhere. Your dev environment, in your pocket. Connects directly to your actual development environment — your code stays on your machine. -**Supported agents:** Claude Code, Codex, GitHub Copilot, OpenCode, Pi, Antigravity, and Muse Code. +**Supported agents:** Claude Code, Codex, GitHub Copilot, OpenCode, Pi, Antigravity, Muse Code, and Hoplite. ## Repository map diff --git a/README.md b/README.md index aa0a841e7e..41c9a8393c 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@

-

One interface for Claude Code, Codex, Copilot, OpenCode, Pi, Antigravity, and Muse Code agents.

+

One interface for Claude Code, Codex, Copilot, OpenCode, Pi, Antigravity, Muse Code, and Hoplite agents.

Paseo app screenshot @@ -42,7 +42,7 @@ Run agents in parallel on your own machines. Ship from your phone or your desk. - **Self-hosted:** Agents run on your machine with your full dev environment. Use your tools, your configs, and your skills. -- **Multi-provider:** Claude Code, Codex, Copilot, OpenCode, Pi, Antigravity, and Muse Code through the same interface. Pick the right model for each job. +- **Multi-provider:** Claude Code, Codex, Copilot, OpenCode, Pi, Antigravity, Muse Code, and Hoplite through the same interface. Pick the right model for each job. - **Voice control:** Dictate tasks or talk through problems in voice mode. Hands-free when you need it. - **Cross-device:** iOS, Android, desktop, web, and CLI. Start work at your desk, check in from your phone, script it from the terminal. - **Privacy-first:** Paseo doesn't have any telemetry, tracking, or forced log-ins. @@ -69,6 +69,7 @@ You need at least one agent CLI installed and configured with your credentials: - [OpenCode](https://github.com/anomalyco/opencode) - [Pi](https://pi.dev) - [Antigravity](https://paseo.sh/docs/supported-providers#antigravity) +- [Hoplite](https://paseo.sh/docs/supported-providers#hoplite) - [Muse Code](https://paseo.sh/docs/muse-code) ### Desktop app (recommended) diff --git a/docs/providers.md b/docs/providers.md index 47c39fd1a8..991de2bab1 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -6,7 +6,7 @@ server integrations. ## Plugin providers Keep a bundled provider in `plugins//` and register it through -`@getpaseo/plugin/server/provider`. Antigravity and Muse Code follow this pattern. Built-in loading and SDK +`@getpaseo/plugin/server/provider`. Antigravity, Muse Code, and Hoplite follow this pattern. Built-in loading and SDK import rules belong to [plugins.md](plugins.md#built-in-plugins); the [public provider guide](../public-docs/plugins/providers.md) covers the provider contract. @@ -19,6 +19,7 @@ provider icon map. You do not need a core manifest entry or provider factory. | ----------- | ------------------------------------------ | ---------------------------------------------------------------- | | Antigravity | Installed `agy` CLI | [Antigravity](../public-docs/supported-providers.md#antigravity) | | Muse Code | MSP over one `muse serve` host per session | [Muse Code](../public-docs/muse-code.md) | +| Hoplite | ACP shim over `hoplite acp` | [Hoplite](../public-docs/supported-providers.md#hoplite) | ## Provider-native session options diff --git a/docs/qa-evidence/hoplite-provider/agent-dark.png b/docs/qa-evidence/hoplite-provider/agent-dark.png new file mode 100644 index 0000000000..f39aedfa5f Binary files /dev/null and b/docs/qa-evidence/hoplite-provider/agent-dark.png differ diff --git a/docs/qa-evidence/hoplite-provider/agent-light.png b/docs/qa-evidence/hoplite-provider/agent-light.png new file mode 100644 index 0000000000..ef0034702e Binary files /dev/null and b/docs/qa-evidence/hoplite-provider/agent-light.png differ diff --git a/docs/qa-evidence/hoplite-provider/new-agent-model-picker.png b/docs/qa-evidence/hoplite-provider/new-agent-model-picker.png new file mode 100644 index 0000000000..6c69da5500 Binary files /dev/null and b/docs/qa-evidence/hoplite-provider/new-agent-model-picker.png differ diff --git a/docs/qa-evidence/hoplite-provider/settings-available.png b/docs/qa-evidence/hoplite-provider/settings-available.png new file mode 100644 index 0000000000..13e7149663 Binary files /dev/null and b/docs/qa-evidence/hoplite-provider/settings-available.png differ diff --git a/docs/qa-evidence/hoplite-provider/settings-unlinked-directory.png b/docs/qa-evidence/hoplite-provider/settings-unlinked-directory.png new file mode 100644 index 0000000000..2a9cfaa026 Binary files /dev/null and b/docs/qa-evidence/hoplite-provider/settings-unlinked-directory.png differ diff --git a/packages/app/src/assets/acp-provider-icons.ts b/packages/app/src/assets/acp-provider-icons.ts index 65f123cb3d..7fb0f3ad1c 100644 --- a/packages/app/src/assets/acp-provider-icons.ts +++ b/packages/app/src/assets/acp-provider-icons.ts @@ -48,6 +48,8 @@ export const ACP_PROVIDER_ICON_SVGS = { goose: '\n \n\n', grok: '\n \n \n\n', + hoplite: + '\n\n\n\n\n', junie: '\n \n \n \n\n', kilo: '\n \n\n', diff --git a/packages/app/src/assets/acp-provider-icons/hoplite.svg b/packages/app/src/assets/acp-provider-icons/hoplite.svg new file mode 100644 index 0000000000..8423442b67 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/hoplite.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/app/src/data/acp-provider-catalog.ts b/packages/app/src/data/acp-provider-catalog.ts index 1fdb1c6f5d..c154c2fa78 100644 --- a/packages/app/src/data/acp-provider-catalog.ts +++ b/packages/app/src/data/acp-provider-catalog.ts @@ -238,6 +238,15 @@ const CATALOG_DATA = [ installLink: "https://hermes-agent.nousresearch.com/docs/user-guide/features/acp", command: ["hermes", "acp"], }, + { + id: "hoplite", + title: "Hoplite", + description: "Hoplite's coding agent through the local hoplite acp bridge", + version: "manual", + iconId: "hoplite", + installLink: "https://hoplite.sh/docs/cli/acp", + command: ["hoplite", "acp"], + }, { id: "junie", title: "Junie", diff --git a/packages/app/src/hooks/use-acp-provider-catalog.test.ts b/packages/app/src/hooks/use-acp-provider-catalog.test.ts index 63e360aba0..c5b77f0f0d 100644 --- a/packages/app/src/hooks/use-acp-provider-catalog.test.ts +++ b/packages/app/src/hooks/use-acp-provider-catalog.test.ts @@ -44,6 +44,7 @@ describe("ACP provider catalog", () => { expect(findProvider("codewhale").command).toEqual(["codewhale", "serve", "--acp"]); expect(findProvider("devin").command).toEqual(["devin", "acp"]); expect(findProvider("goose").command).toEqual(["goose", "acp"]); + expect(findProvider("hoplite").command).toEqual(["hoplite", "acp"]); expect(findProvider("junie").command).toEqual(["junie", "--acp", "true"]); expect(findProvider("kiro").command).toEqual(["kiro-cli", "acp"]); expect(findProvider("poolside").command).toEqual(["pool", "acp"]); @@ -59,6 +60,15 @@ describe("ACP provider catalog", () => { expect(findProvider("minimax-code").iconSvg).toContain(" { + expect(findProvider("hoplite")).toMatchObject({ + title: "Hoplite", + command: ["hoplite", "acp"], + installLink: "https://hoplite.sh/docs/cli/acp", + }); + expect(findProvider("hoplite").iconSvg).toContain('fill="currentColor"'); + }); + it("maps a catalog entry to the daemon provider config patch", () => { expect(buildAcpProviderConfigPatch(findProvider("amp-acp"))).toEqual({ providers: { diff --git a/packages/server/src/server/daemon-e2e/models.e2e.test.ts b/packages/server/src/server/daemon-e2e/models.e2e.test.ts index 1f2086c879..7c70fce315 100644 --- a/packages/server/src/server/daemon-e2e/models.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/models.e2e.test.ts @@ -165,3 +165,52 @@ test("Antigravity creates an agent and answers a prompt with real agy", async ({ await rm(cwd, { recursive: true, force: true }); } }, 120000); + +// Hoplite selects its project from the GitHub repository of the session directory, so this test +// needs a checkout of a repository linked to a Hoplite project. +test("Hoplite creates an agent and answers a prompt with real hoplite acp", async ({ skip }) => { + const command = await findExecutable(process.env.HOPLITE_COMMAND ?? "hoplite"); + const cwd = process.env.HOPLITE_E2E_CWD; + if (command === null || !cwd) { + skip("hoplite is not resolvable or HOPLITE_E2E_CWD is unset"); + return; + } + const daemon = await createTestPaseoDaemon({ + agentClients: {}, + mcpEnabled: false, + builtinPlugins: new BuiltinPluginLoader(undefined, ["hoplite-provider"]), + providerOverrides: { hoplite: { command: [command, "acp"], paseoTools: { enabled: false } } }, + }); + const client = new DaemonClient({ + url: `ws://127.0.0.1:${daemon.port}/ws`, + appVersion: "0.11.0", + }); + try { + await client.connect(); + await client.fetchAgents({ subscribe: {} }); + await expect + .poll( + async () => { + const snapshot = await client.getProvidersSnapshot({ cwd }); + return snapshot.entries.find((entry) => entry.provider === "hoplite")?.status; + }, + { timeout: 60000 }, + ) + .toBe("ready"); + const agent = await client.createAgent({ provider: "hoplite", cwd, title: "Hoplite E2E" }); + await client.sendMessage(agent.id, "Reply with exactly HOPLITE_E2E_OK. No tools."); + const result = await client.waitForFinish(agent.id, 180000); + expect(result.status).toBe("idle"); + const items = (await client.fetchAgentTimeline(agent.id, { limit: 100 })).entries.map( + (entry) => entry.item, + ); + expect( + items.some( + (item) => item.type === "assistant_message" && item.text.includes("HOPLITE_E2E_OK"), + ), + ).toBe(true); + } finally { + await client.close(); + await daemon.close(); + } +}, 240000); diff --git a/packages/server/src/server/plugins/builtin/index.ts b/packages/server/src/server/plugins/builtin/index.ts index c14c027853..cf6ffe9eee 100644 --- a/packages/server/src/server/plugins/builtin/index.ts +++ b/packages/server/src/server/plugins/builtin/index.ts @@ -9,6 +9,7 @@ export const builtinPlugins = [ "copilot-usage-source", "cursor-usage-source", "grok-usage-source", + "hoplite-provider", "kimi-usage-source", "minimax-usage-source", "muse-provider", diff --git a/plugins/hoplite-provider/icon.svg b/plugins/hoplite-provider/icon.svg new file mode 100644 index 0000000000..5d3765c39f --- /dev/null +++ b/plugins/hoplite-provider/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/plugins/hoplite-provider/index.server.ts b/plugins/hoplite-provider/index.server.ts new file mode 100644 index 0000000000..4c101df470 --- /dev/null +++ b/plugins/hoplite-provider/index.server.ts @@ -0,0 +1,7 @@ +import type { PluginServerContext } from "@getpaseo/plugin/server"; +import { createHopliteProvider } from "./server/provider.js"; + +export default function contribute(server: PluginServerContext) { + server.registerProvider(createHopliteProvider()); + return () => {}; +} diff --git a/plugins/hoplite-provider/paseo-plugin.json b/plugins/hoplite-provider/paseo-plugin.json new file mode 100644 index 0000000000..138cf83ad6 --- /dev/null +++ b/plugins/hoplite-provider/paseo-plugin.json @@ -0,0 +1 @@ +{ "id": "hoplite-provider", "requirements": { "paseo": ">=0.11.0" } } diff --git a/plugins/hoplite-provider/server/provider.ts b/plugins/hoplite-provider/server/provider.ts new file mode 100644 index 0000000000..02b73e73bd --- /dev/null +++ b/plugins/hoplite-provider/server/provider.ts @@ -0,0 +1,14 @@ +import { runAcpProvider } from "@getpaseo/plugin/server/acp"; +import type { ProviderRegistration } from "@getpaseo/plugin/server/provider"; +import { runPhaseTransformer } from "./run-phase.js"; + +export function createHopliteProvider(): ProviderRegistration { + return runAcpProvider({ + id: "hoplite", + label: "Hoplite", + description: "Hoplite's coding agent through the local `hoplite acp` bridge", + icon: "icon.svg", + command: ["hoplite", "acp"], + transformers: [runPhaseTransformer], + }); +} diff --git a/plugins/hoplite-provider/server/run-phase.test.ts b/plugins/hoplite-provider/server/run-phase.test.ts new file mode 100644 index 0000000000..5526f386c6 --- /dev/null +++ b/plugins/hoplite-provider/server/run-phase.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; +import { runPhaseTransformer } from "./run-phase.js"; + +const context = { sessionId: "session-1" }; + +// Frames captured from `hoplite acp` 3.0.0. +function runPhase(phase: string | null, label: string | null) { + return { + method: "_hoplite/run_phase", + params: { + sessionId: "thr_c75d39f7d7624256af918eeee2f4941b", + runId: "run_eb213b97bd3147afb2927fbc27f87180", + phase, + label, + startedAt: phase === null ? null : "2026-10-04T17:51:46.311Z", + }, + }; +} + +describe("runPhaseTransformer", () => { + it("shows a provisioning phase as one running workspace item per run", () => { + const update = runPhaseTransformer.notification?.( + runPhase("fetching_repository", "Cloning repository"), + context, + ); + + expect(update).toEqual({ + type: "timeline", + item: { + type: "tool_call", + id: "hoplite-run-phase:run_eb213b97bd3147afb2927fbc27f87180", + callId: "hoplite-run-phase:run_eb213b97bd3147afb2927fbc27f87180", + name: "Hoplite workspace", + detail: { type: "plain_text", label: "Cloning repository", icon: "wrench" }, + status: "running", + error: null, + }, + }); + }); + + it("completes the same item when the run leaves provisioning", () => { + const update = runPhaseTransformer.notification?.(runPhase(null, null), context); + + expect(update).toMatchObject({ + type: "timeline", + item: { + id: "hoplite-run-phase:run_eb213b97bd3147afb2927fbc27f87180", + status: "completed", + detail: { label: "Workspace ready" }, + }, + }); + }); + + it("falls back to the phase name when Hoplite sends no label", () => { + const update = runPhaseTransformer.notification?.( + runPhase("configuring_workspace", null), + context, + ); + + expect(update).toMatchObject({ item: { detail: { label: "configuring_workspace" } } }); + }); + + it("ignores other vendor notifications and malformed phases", () => { + expect( + runPhaseTransformer.notification?.({ method: "_hoplite/other", params: {} }, context), + ).toBeNull(); + expect( + runPhaseTransformer.notification?.( + { method: "_hoplite/run_phase", params: { phase: "x" } }, + context, + ), + ).toBeNull(); + }); +}); diff --git a/plugins/hoplite-provider/server/run-phase.ts b/plugins/hoplite-provider/server/run-phase.ts new file mode 100644 index 0000000000..767ee0447d --- /dev/null +++ b/plugins/hoplite-provider/server/run-phase.ts @@ -0,0 +1,39 @@ +import type { AcpTransformer } from "@getpaseo/plugin/server/acp"; +import { z } from "zod"; + +// Hoplite provisions a cloud workspace before the first agent output of a run and reports each +// step as `_hoplite/run_phase`. A null phase ends provisioning for that run. +const runPhaseSchema = z.object({ + runId: z.string().min(1), + phase: z.string().nullable(), + label: z.string().nullable(), +}); + +const RUN_PHASE_METHOD = "_hoplite/run_phase"; + +export const runPhaseTransformer: AcpTransformer = { + notification(notification) { + if (notification.method !== RUN_PHASE_METHOD) return null; + const params = runPhaseSchema.safeParse(notification.params); + if (!params.success) return null; + const { runId, phase, label } = params.data; + const id = `hoplite-run-phase:${runId}`; + const running = phase !== null; + return { + type: "timeline", + item: { + type: "tool_call", + id, + callId: id, + name: "Hoplite workspace", + detail: { + type: "plain_text", + label: running ? (label ?? phase) : "Workspace ready", + icon: "wrench", + }, + status: running ? "running" : "completed", + error: null, + }, + }; + }, +}; diff --git a/public-docs/providers.md b/public-docs/providers.md index 6de304a019..9e80ebecde 100644 --- a/public-docs/providers.md +++ b/public-docs/providers.md @@ -8,7 +8,7 @@ category: Providers # Providers -Paseo doesn't ship its own coding agent. It launches and supervises **existing CLIs you've already installed and authenticated**, Claude Code, Codex, OpenCode, Antigravity, Muse Code, Cursor, Gemini, and the rest. Your subscriptions, your config, your skills, your MCP servers all stay intact. Paseo just gives you a UI, a CLI, a relay, and orchestration on top. +Paseo doesn't ship its own coding agent. It launches and supervises **existing CLIs you've already installed and authenticated**, Claude Code, Codex, OpenCode, Antigravity, Muse Code, Hoplite, Cursor, Gemini, and the rest. Your subscriptions, your config, your skills, your MCP servers all stay intact. Paseo just gives you a UI, a CLI, a relay, and orchestration on top. ## Mental model @@ -16,7 +16,7 @@ A provider is the contract between Paseo and one external agent CLI: how to laun ## Two tiers -- **Native support**, Paseo ships a bundled adapter for the major agents (Claude Code, Codex, OpenCode, Pi, [Antigravity](/docs/supported-providers#antigravity), [Muse Code](/docs/muse-code)). Auto-discovered when the underlying CLI is installed, with mode metadata and voice support where applicable. +- **Native support**, Paseo ships a bundled adapter for the major agents (Claude Code, Codex, OpenCode, Pi, [Antigravity](/docs/supported-providers#antigravity), [Muse Code](/docs/muse-code), [Hoplite](/docs/supported-providers#hoplite)). Auto-discovered when the underlying CLI is installed, with mode metadata and voice support where applicable. - **ACP catalog**, any agent speaking the [Agent Client Protocol](https://agentclientprotocol.com) is supported through a generic adapter. Paseo ships a curated catalog of one-click installs (Cursor, Gemini, GitHub Copilot, Hermes, Kimi, Qwen Code, and 25+ more), and you can add any other ACP agent yourself. Either way, **you install the underlying CLI**. Paseo runs it. diff --git a/public-docs/supported-providers.md b/public-docs/supported-providers.md index f2b1370b68..a958276224 100644 --- a/public-docs/supported-providers.md +++ b/public-docs/supported-providers.md @@ -20,6 +20,7 @@ Work out of the box once the underlying CLI is installed and authenticated. - [Pi](https://pi.dev). Minimal terminal-based coding agent with multi-provider LLM support. - [Antigravity](#antigravity). Uses your installed `agy` CLI through a built-in provider plugin. - [Muse Code](/docs/muse-code). Meta's terminal coding agent, bundled as a provider plugin. +- [Hoplite](#hoplite). Uses your installed `hoplite` CLI through a built-in provider plugin. ### Antigravity @@ -58,6 +59,37 @@ for provider-specific environment variables: See [Custom providers](/docs/custom-providers) for command and environment overrides. +### Hoplite + +Install the Hoplite CLI on your daemon host with `brew install carboncopyinc/tap/hoplite`, then run +`hoplite login`. Pick **Hoplite** in Paseo. The provider ships with Paseo and talks to Hoplite +through `hoplite acp`; no separate plugin install is required. + +Hoplite runs the agent in a cloud workspace for a Hoplite project. The project comes from the +GitHub repository of the agent's directory, so open a repository that is linked to a Hoplite +project. In any other directory Hoplite reports `No Hoplite project`, and the Hoplite row in +**Settings → Providers** shows that error for the daemon's own directory. To use one project +everywhere, set its ID: + +```json +{ + "agents": { + "providers": { + "hoplite": { + "env": { "HOPLITE_PROJECT_ID": "your-project-id" } + } + } + } +} +``` + +Models come from your Hoplite organization. While Hoplite prepares the workspace, the chat shows +one **Hoplite workspace** row with the current step. Conversations resume after a daemon restart. +Paseo MCP tools are unavailable because the agent runs in Hoplite's workspace, not on your host; +Hoplite's own tools, including its subagents, still work. + +Override `command` if `hoplite` is outside your daemon's PATH. Keep `acp` in the arguments. + ## ACP catalog Pick any of these from the in-app provider catalog. Each entry is a one-click install of the underlying CLI.