feat: report Pi and OpenCode in guided setup (#109624)

* feat(onboarding): detect Pi and OpenCode CLIs

* fix(onboarding): clarify Pi and OpenCode readiness
This commit is contained in:
Peter Steinberger
2026-07-16 22:35:33 -07:00
committed by GitHub
parent a1dfd47edd
commit 399345e1bb
6 changed files with 81 additions and 14 deletions
@@ -709,17 +709,17 @@ struct OnboardingAISetupTests {
#expect(local["modelRef"]?.value as? String == "lmstudio/qwen-local")
}
@Test func `unavailable detected integration decodes for informational display`() throws {
let candidate = try JSONDecoder().decode(
OnboardingAISetupModel.UnavailableCandidate.self,
@Test func `unavailable detected integrations decode for informational display`() throws {
let candidates = try JSONDecoder().decode(
[OnboardingAISetupModel.UnavailableCandidate].self,
from: Data(
#"{"id":"antigravity-cli","label":"Antigravity CLI","detail":"installed","reason":"Automatic setup cannot enforce a tool-free probe."}"#.utf8
#"[{"id":"pi-cli","label":"Pi CLI","detail":"installed","reason":"Not a setup route."},{"id":"opencode-cli","label":"OpenCode CLI","detail":"installed","reason":"Not a setup route."}]"#.utf8
)
)
#expect(candidate.id == "antigravity-cli")
#expect(candidate.label == "Antigravity CLI")
#expect(candidate.detail == "installed")
#expect(candidates.map(\.id) == ["pi-cli", "opencode-cli"])
#expect(candidates.map(\.label) == ["Pi CLI", "OpenCode CLI"])
#expect(candidates.allSatisfy { $0.detail == "installed" })
}
@Test func `activation decodes and retains copyable setup lines`() throws {
+2
View File
@@ -38,6 +38,8 @@ automatic pass. Detected local runtimes are auto-tested after CLI and API-key
candidates; when several local models are available, OpenClaw prefers the
strongest tool-calling instruct family. The selected candidate must answer a
real completion before its provider and model configuration is saved.
Installed Gemini, Antigravity, Pi, and OpenCode CLIs are also reported when
they cannot serve as the reusable inference route for guided setup.
`setup` accepts the same onboarding flags as `openclaw onboard`, including
auth (`--auth-choice`, `--token`, provider key flags), Gateway
+5 -3
View File
@@ -85,9 +85,11 @@ To use a Claude subscription when the Gateway host has no Claude CLI login, run
printed token as **Anthropic setup-token** under **Connect with an API key or
token**.
Gemini CLI and Antigravity remain available for normal use after setup. Their
installed CLIs are shown for context but are not auto-tested because neither can
enforce the tool-free inference probe.
Installed Gemini CLI, Antigravity, Pi, and OpenCode CLIs are shown for context
when they cannot be selected as the reusable guided-setup inference route.
Gemini and Antigravity cannot enforce the tool-free inference probe. Pi and
OpenCode are whole-agent harnesses rather than setup inference routes; their
session integrations require separate runtime and plugin setup.
You can also sign in through the provider's own OAuth or device-pairing flow.
The built-in choices include OpenAI/ChatGPT, OpenRouter, GitHub Copilot, Google
+4 -2
View File
@@ -76,8 +76,10 @@ Plain `openclaw onboard` follows this path:
2. Detect configured models, API-key environment variables, supported local AI
CLIs, and already installed tool-capable models from reachable Ollama or LM
Studio servers on the Gateway host. This read-only pass never downloads a
model. Gemini CLI and Antigravity installs are reported but not auto-tested
because they cannot enforce a tool-free probe.
model. Gemini CLI, Antigravity, Pi, and OpenCode installs are also reported
when they cannot serve as the reusable inference route for guided setup.
Gemini and Antigravity cannot enforce the tool-free probe; Pi and OpenCode
are whole-agent harnesses rather than setup inference routes.
3. Test the first detected candidate with a real completion. On failure, show the
reason and continue to the next usable candidate.
4. If detection is exhausted, choose OpenAI, Anthropic, xAI (Grok), Google, or
+39 -1
View File
@@ -406,7 +406,10 @@ describe("detectSetupInference", () => {
credentials: false,
},
],
probeLocalCommand: vi.fn(async (command) => ({ command, found: command === "agy" })),
probeLocalCommand: vi.fn(async (command) => ({
command,
found: command === "agy" || command === "pi" || command === "opencode",
})),
resolveManifestProviderAuthChoices: () => [
{
pluginId: "local-plugin",
@@ -435,6 +438,8 @@ describe("detectSetupInference", () => {
expect(detection.unavailableCandidates).toEqual([
expect.objectContaining({ id: "gemini-cli" }),
expect.objectContaining({ id: "antigravity-cli" }),
expect.objectContaining({ id: "pi-cli" }),
expect.objectContaining({ id: "opencode-cli" }),
]);
expect(detect).toHaveBeenCalledOnce();
expect(prepare).not.toHaveBeenCalled();
@@ -677,6 +682,39 @@ describe("detectSetupInference", () => {
expect.objectContaining({ id: "gemini-cli" }),
]);
});
it("reports installed Pi and OpenCode without offering them as setup inference routes", async () => {
vi.mocked(detectInferenceBackends).mockResolvedValueOnce([]);
const probeLocalCommand = vi.fn(async (command: string) => ({
command,
found: command === "pi" || command === "opencode",
}));
const detection = await detectSetupInference({
resolveManifestProviderAuthChoices: () => [],
probeLocalCommand,
});
expect(detection.candidates).toEqual([]);
expect(detection.unavailableCandidates).toEqual([
{
id: "pi-cli",
label: "Pi CLI",
detail: "installed",
reason:
"Pi CLI is installed, but its whole-agent sessions require separate setup and are not a reusable guided-setup inference route.",
},
{
id: "opencode-cli",
label: "OpenCode CLI",
detail: "installed",
reason:
"OpenCode CLI is installed, but its ACP harness requires separate setup and is not a reusable guided-setup inference route.",
},
]);
expect(probeLocalCommand).toHaveBeenCalledWith("pi");
expect(probeLocalCommand).toHaveBeenCalledWith("opencode");
});
});
async function runCodexSetupWithFinalConfig(params: {
+24 -1
View File
@@ -365,7 +365,12 @@ export async function detectSetupInference(
reason:
"Can't be auto-tested safely here. Use 'Gemini CLI OAuth' or a Gemini API key instead.",
}));
const antigravity = await (deps.probeLocalCommand ?? probeLocalCommand)("agy");
const probe = deps.probeLocalCommand ?? probeLocalCommand;
const [antigravity, pi, opencode] = await Promise.all([
probe("agy"),
probe("pi"),
probe("opencode"),
]);
if (antigravity.found) {
unavailableCandidates.push({
id: "antigravity-cli",
@@ -375,6 +380,24 @@ export async function detectSetupInference(
"Can't be auto-tested safely here. Sign in with a provider or use an API key instead.",
});
}
if (pi.found) {
unavailableCandidates.push({
id: "pi-cli",
label: "Pi CLI",
detail: "installed",
reason:
"Pi CLI is installed, but its whole-agent sessions require separate setup and are not a reusable guided-setup inference route.",
});
}
if (opencode.found) {
unavailableCandidates.push({
id: "opencode-cli",
label: "OpenCode CLI",
detail: "installed",
reason:
"OpenCode CLI is installed, but its ACP harness requires separate setup and is not a reusable guided-setup inference route.",
});
}
const raw = detected.filter((candidate) => candidate.kind !== "gemini-cli");
const candidates: SetupInferenceCandidate[] = raw.map((candidate) =>
// Released macOS clients require this field. Keep it false so the wire