fix(desktop): honor selected onboarding runtime config (#2047)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Wes
2026-07-17 15:58:49 -07:00
committed by GitHub
co-authored by Pinky
parent 3aba3a5316
commit 0bbcafe9e6
8 changed files with 166 additions and 71 deletions
@@ -180,7 +180,7 @@ export function GlobalAgentConfigEditor({
) : (
<GlobalAgentConfigFields
bakedEnv={bakedEnv}
buzzAgentRuntime={buzzAgentRuntime}
selectedRuntime={buzzAgentRuntime}
config={config}
isCustomModelEditing={isCustomModelEditing}
isCustomProvider={isCustomProvider}
@@ -57,7 +57,7 @@ const BAKED_STRUCTURED_KEYS = new Set([
export type GlobalAgentConfigFieldsProps = {
bakedEnv: BakedEnvEntry[];
buzzAgentRuntime: AcpRuntimeCatalogEntry | undefined;
selectedRuntime: AcpRuntimeCatalogEntry | undefined;
config: GlobalAgentConfig;
isCustomModelEditing: boolean;
isCustomProvider: boolean;
@@ -69,7 +69,7 @@ export type GlobalAgentConfigFieldsProps = {
export function GlobalAgentConfigFields({
bakedEnv,
buzzAgentRuntime,
selectedRuntime,
config,
isCustomModelEditing,
isCustomProvider,
@@ -134,7 +134,7 @@ export function GlobalAgentConfigFields({
modelFieldVisible: true,
open: true,
provider: providerForDiscovery,
selectedRuntime: buzzAgentRuntime,
selectedRuntime,
});
const currentEffortForAutoClear =
@@ -80,12 +80,19 @@ function AgentDefaultsSection() {
};
}, []);
const buzzAgentRuntime = React.useMemo(
() => (runtimesQuery.data ?? []).find((r) => r.id === "buzz-agent"),
[runtimesQuery.data],
const selectedRuntime = React.useMemo(
() =>
(runtimesQuery.data ?? []).find(
(runtime) => runtime.id === config.preferred_runtime,
),
[config.preferred_runtime, runtimesQuery.data],
);
const readiness = resolveAgentReadiness(runtimesQuery.data ?? [], config);
const readiness = resolveAgentReadiness(
runtimesQuery.data ?? [],
config,
"preferred",
);
return (
<section className="w-full space-y-4 text-left">
@@ -98,7 +105,7 @@ function AgentDefaultsSection() {
<div className="rounded-2xl bg-white/85 p-2 shadow-[0_0_55px_25px_rgba(255,255,255,0.6)]">
<GlobalAgentConfigFields
bakedEnv={bakedEnv}
buzzAgentRuntime={buzzAgentRuntime}
selectedRuntime={selectedRuntime}
config={config}
isCustomModelEditing={isCustomModelEditing}
isCustomProvider={isCustomProvider}
@@ -30,6 +30,7 @@ function makeConfig(overrides = {}) {
env_vars: {},
provider: null,
model: null,
preferred_runtime: "goose",
...overrides,
};
}
@@ -38,27 +39,26 @@ function makeConfig(overrides = {}) {
// CLI path
// ---------------------------------------------------------------------------
test("resolveAgentReadiness_cli_returns_ready_when_cli_runtime_available_and_logged_in", () => {
const runtimes = [makeRuntime({ id: "goose", label: "Goose" })];
const result = resolveAgentReadiness(runtimes, makeConfig());
test("resolveAgentReadiness_cli_returns_ready_when_preferred_cli_runtime_is_logged_in", () => {
const runtimes = [makeRuntime({ id: "claude", label: "Claude" })];
const result = resolveAgentReadiness(
runtimes,
makeConfig({ preferred_runtime: "claude" }),
);
assert.deepEqual(result, {
ready: true,
reason: "cli",
runtimeLabel: "Goose",
runtimeLabel: "Claude",
});
});
test("resolveAgentReadiness_cli_uses_first_matching_runtime", () => {
test("resolveAgentReadiness_uses_only_the_preferred_runtime", () => {
const runtimes = [
makeRuntime({ id: "claude", label: "Claude" }),
makeRuntime({ id: "goose", label: "Goose" }),
];
const result = resolveAgentReadiness(runtimes, makeConfig());
assert.equal(result.ready, true);
if (result.ready) {
assert.equal(result.reason, "cli");
assert.equal(result.runtimeLabel, "Claude");
}
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
assert.equal(result.ready, false);
});
test("resolveAgentReadiness_cli_skips_logged_out_runtimes", () => {
@@ -69,13 +69,11 @@ test("resolveAgentReadiness_cli_skips_logged_out_runtimes", () => {
authStatus: { status: "logged_out" },
}),
];
const result = resolveAgentReadiness(runtimes, makeConfig());
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
assert.equal(result.ready, false);
});
test("resolveAgentReadiness_cli_ready_for_login_free_harness_with_not_applicable_auth", () => {
// Goose uses not_applicable because it has no login concept; it should
// still show green if the runtime is available.
test("resolveAgentReadiness_goose_requires_provider_and_model", () => {
const runtimes = [
makeRuntime({
id: "goose",
@@ -84,12 +82,8 @@ test("resolveAgentReadiness_cli_ready_for_login_free_harness_with_not_applicable
authStatus: { status: "not_applicable" },
}),
];
const result = resolveAgentReadiness(runtimes, makeConfig());
assert.deepEqual(result, {
ready: true,
reason: "cli",
runtimeLabel: "Goose",
});
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
assert.equal(result.ready, false);
});
test("resolveAgentReadiness_cli_not_ready_for_unknown_auth_status", () => {
@@ -102,7 +96,7 @@ test("resolveAgentReadiness_cli_not_ready_for_unknown_auth_status", () => {
authStatus: { status: "unknown" },
}),
];
const result = resolveAgentReadiness(runtimes, makeConfig());
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
assert.equal(result.ready, false);
});
@@ -115,7 +109,7 @@ test("resolveAgentReadiness_cli_not_ready_for_config_invalid_auth_status", () =>
authStatus: { status: "config_invalid" },
}),
];
const result = resolveAgentReadiness(runtimes, makeConfig());
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
assert.equal(result.ready, false);
});
@@ -128,7 +122,7 @@ test("resolveAgentReadiness_cli_skips_unavailable_runtimes", () => {
authStatus: { status: "logged_in" },
}),
];
const result = resolveAgentReadiness(runtimes, makeConfig());
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
assert.equal(result.ready, false);
});
@@ -141,7 +135,7 @@ test("resolveAgentReadiness_cli_ignores_buzz_agent_runtime", () => {
authStatus: { status: "not_applicable" },
}),
];
const result = resolveAgentReadiness(runtimes, makeConfig());
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
assert.equal(result.ready, false);
});
@@ -151,12 +145,15 @@ test("resolveAgentReadiness_cli_ignores_buzz_agent_runtime", () => {
test("resolveAgentReadiness_buzz_agent_ready_when_provider_model_and_key_set", () => {
// anthropic requires ANTHROPIC_API_KEY
const config = makeConfig({
provider: "anthropic",
model: "claude-3-5-sonnet-latest",
env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" },
});
const result = resolveAgentReadiness([], config);
const result = resolveAgentReadiness(
[makeRuntime({ id: "buzz-agent", label: "Buzz Agent" })],
makeConfig({
preferred_runtime: "buzz-agent",
provider: "anthropic",
model: "claude-3-5-sonnet-latest",
env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" },
}),
);
assert.deepEqual(result, { ready: true, reason: "buzz-agent" });
});
@@ -199,20 +196,59 @@ test("resolveAgentReadiness_neither_returns_not_ready", () => {
assert.deepEqual(result, { ready: false });
});
test("resolveAgentReadiness_welcome_readiness_uses_ready_cli_without_preference", () => {
const runtimes = [makeRuntime({ id: "claude", label: "Claude" })];
const result = resolveAgentReadiness(
runtimes,
makeConfig({ preferred_runtime: null }),
);
assert.deepEqual(result, {
ready: true,
reason: "cli",
runtimeLabel: "Claude",
});
});
test("resolveAgentReadiness_legacy_config_without_preference_uses_buzz_agent_fields", () => {
const runtimes = [makeRuntime({ id: "buzz-agent", label: "Buzz Agent" })];
const result = resolveAgentReadiness(
runtimes,
makeConfig({
preferred_runtime: null,
provider: "anthropic",
model: "claude-3-5-sonnet-latest",
env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" },
}),
);
assert.deepEqual(result, { ready: true, reason: "buzz-agent" });
});
test("resolveAgentReadiness_legacy_config_does_not_treat_goose_binary_as_ready", () => {
const result = resolveAgentReadiness(
[makeRuntime({ id: "goose", label: "Goose" })],
makeConfig({ preferred_runtime: null }),
"preferred",
);
assert.deepEqual(result, { ready: false });
});
// ---------------------------------------------------------------------------
// CLI path takes priority over buzz-agent path
// Preferred runtime isolation
// ---------------------------------------------------------------------------
test("resolveAgentReadiness_cli_wins_over_buzz_agent_when_both_ready", () => {
const runtimes = [makeRuntime({ id: "goose", label: "Goose" })];
const config = makeConfig({
provider: "anthropic",
model: "claude-3-5-sonnet-latest",
env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" },
});
const result = resolveAgentReadiness(runtimes, config);
assert.equal(result.ready, true);
if (result.ready) {
assert.equal(result.reason, "cli");
}
test("resolveAgentReadiness_preferred_goose_does_not_borrow_ready_buzz_agent_config", () => {
const runtimes = [
makeRuntime({ id: "goose", label: "Goose" }),
makeRuntime({ id: "buzz-agent", label: "Buzz Agent" }),
];
const result = resolveAgentReadiness(
runtimes,
makeConfig({
provider: "anthropic",
model: null,
env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" },
}),
"preferred",
);
assert.equal(result.ready, false);
});
@@ -12,35 +12,60 @@ export type AgentReadinessResult =
/**
* Determine whether the user has a working agent path configured.
*
* CLI path: at least one non-buzz-agent runtime is available and logged in.
* buzz-agent path: provider and model are set, and all required credential
* env vars for that provider are present.
* CLI path: the preferred Claude or Codex runtime is available and logged in.
* Provider path: the preferred Buzz Agent or Goose runtime has provider and
* model set, plus all required credential env vars for that provider.
*
* Returns enough info for the UI to say which path matched, or that neither did.
*/
export function resolveAgentReadiness(
runtimes: readonly AcpRuntimeCatalogEntry[],
globalConfig: GlobalAgentConfig,
scope: "any" | "preferred" = "any",
): AgentReadinessResult {
// CLI path — any non-buzz-agent runtime that is available and has an
// auth status indicating it can run: logged_in for runtimes that require
// auth (Claude Code), or not_applicable for login-free harnesses (Goose).
for (const runtime of runtimes) {
if (runtime.id === "buzz-agent") continue;
if (
runtime.availability === "available" &&
(runtime.authStatus.status === "logged_in" ||
runtime.authStatus.status === "not_applicable")
) {
return { ready: true, reason: "cli", runtimeLabel: runtime.label };
if (scope === "any") {
for (const runtime of runtimes) {
if (runtime.id === "buzz-agent") continue;
if (
runtime.availability === "available" &&
(runtime.authStatus.status === "logged_in" ||
runtime.authStatus.status === "not_applicable")
) {
return { ready: true, reason: "cli", runtimeLabel: runtime.label };
}
}
}
// buzz-agent path — provider + model + required credential keys all present.
const preferredRuntime =
scope === "preferred"
? runtimes.find(
(runtime) => runtime.id === globalConfig.preferred_runtime,
)
: runtimes.find((runtime) => runtime.id === "buzz-agent");
if (preferredRuntime?.availability !== "available") {
return { ready: false };
}
if (
(preferredRuntime.id === "claude" || preferredRuntime.id === "codex") &&
(preferredRuntime.authStatus.status === "logged_in" ||
preferredRuntime.authStatus.status === "not_applicable")
) {
return {
ready: true,
reason: "cli",
runtimeLabel: preferredRuntime.label,
};
}
if (preferredRuntime.id !== "buzz-agent" && preferredRuntime.id !== "goose") {
return { ready: false };
}
const provider = globalConfig.provider?.trim() ?? "";
const model = globalConfig.model?.trim() ?? "";
if (provider.length > 0 && model.length > 0) {
const required = requiredCredentialEnvKeys("buzz-agent", provider);
const required = requiredCredentialEnvKeys(preferredRuntime.id, provider);
const allKeysPresent = required.every(
(key) => (globalConfig.env_vars[key] ?? "").trim().length > 0,
);
@@ -242,7 +242,7 @@ test("readiness wait returns the subset that became online by the deadline", asy
[bumble.pubkey]: "offline",
}),
pollMs: 0,
waitMs: 1,
waitMs: 0,
});
assert.deepEqual(online, [honey]);
@@ -186,7 +186,7 @@ export async function waitForWelcomeTeammatesOnline(
const pubkeys = teammates.map((agent) => agent.pubkey);
let latestOnline: ManagedAgent[] = [];
while (!options.isCancelled() && Date.now() < deadline) {
while (!options.isCancelled()) {
try {
latestOnline = onlineWelcomeTeammates(
teammates,
@@ -198,6 +198,7 @@ export async function waitForWelcomeTeammatesOnline(
} catch (error) {
console.warn("Welcome teammate presence check failed; retrying.", error);
}
if (Date.now() >= deadline) break;
await new Promise((resolve) => globalThis.setTimeout(resolve, pollMs));
}
return options.isCancelled() ? [] : latestOnline;
@@ -375,6 +375,32 @@ test("Finish button is always enabled on config page regardless of readiness", a
// B1 regression: rapid consecutive edits must not lose the later change
// ---------------------------------------------------------------------------
test("Goose config page discovers models through the selected Goose runtime", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
availableRuntime("goose", { status: "not_applicable" }),
],
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-runtime-goose").click();
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
await page.locator("#global-agent-provider").selectOption("openai");
await expect(
page
.locator("#global-agent-model")
.getByRole("option", { name: "GPT-5.5" }),
).toBeAttached();
});
test("provider credentials are first-class and drive model discovery", async ({
page,
}) => {