Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion desktop/src/features/agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ with a TypeScript lookup table or an id comparison in a component.
7. **Onboarding setup detects readiness; it does not select defaults.** The
setup page derives visible and ready harnesses from the runtime catalog and
only offers install or sign-in actions. The following defaults page is the
sole onboarding surface that chooses and persists `preferred_runtime`.
sole onboarding surface that chooses and persists `preferred_runtime`, and
its Finish gate consumes the shared renderer's `onValidityChange` signal —
a harness selection alone does not complete onboarding when the harness
requires provider/model/credential config (e.g. buzz-agent with no
provider). Baked build env and runtime-file config satisfy the gate.
`onboarding-agent-defaults.spec.ts` is the acceptance gate for anything
touching this flow or the shared renderer.
8. **Omit the Model control only after a confirmed successful empty
Expand Down
15 changes: 13 additions & 2 deletions desktop/src/features/onboarding/ui/DefaultConfigStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function AgentDefaultsSection({
cancel: () => void;
} | null>(null);
const [isSaving, setIsSaving] = React.useState(false);
const [configIsValid, setConfigIsValid] = React.useState(false);

React.useEffect(() => {
let unmounted = false;
Expand Down Expand Up @@ -187,10 +188,19 @@ function AgentDefaultsSection({
);
React.useEffect(() => {
onPersistenceStateChange({
canComplete: selectedRuntimeId.length > 0 && !isSaving,
// configIsValid comes from AgentConfigFields' onValidityChange and
// covers model + provider credentials — a harness selection alone is
// not a working default (e.g. buzz-agent with no provider configured).
canComplete: selectedRuntimeId.length > 0 && configIsValid && !isSaving,
flush: flushPersistence,
});
}, [flushPersistence, isSaving, onPersistenceStateChange, selectedRuntimeId]);
}, [
configIsValid,
flushPersistence,
isSaving,
onPersistenceStateChange,
selectedRuntimeId,
]);

return (
<section className="w-full space-y-4 text-left text-sm">
Expand Down Expand Up @@ -239,6 +249,7 @@ function AgentDefaultsSection({
}}
onCustomModelEditingChange={setIsCustomModelEditing}
onIsCustomProviderChange={setIsCustomProvider}
onValidityChange={setConfigIsValid}
placeholderClassName="text-foreground/70"
runtimeFileConfig={runtimeFileConfig}
selectClassName="h-12 rounded-2xl border-foreground/15 bg-white px-4 py-2 text-sm shadow-none hover:bg-white/95"
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/onboarding/ui/SetupStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ function RuntimeProvidersSection({

<div className="flex w-full flex-1 flex-col items-center justify-center gap-8 py-10">
{orderedItems.length > 0 ? (
<div className="grid min-w-0 w-full max-w-[592px] grid-cols-1 gap-4 md:grid-cols-2">
<div className="grid min-w-0 w-full max-w-[1200px] grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
{orderedItems.map((runtime) => (
<RuntimeCard
installResults={installResults}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ function runtime(id, availability, status) {
return { id, availability, authStatus: { status } };
}

test("only Claude Code and Codex are visible in onboarding", () => {
test("all bundled harnesses are visible in onboarding", () => {
assert.equal(runtimeIsVisibleInOnboarding("claude"), true);
assert.equal(runtimeIsVisibleInOnboarding("codex"), true);
assert.equal(runtimeIsVisibleInOnboarding("goose"), false);
assert.equal(runtimeIsVisibleInOnboarding("buzz-agent"), false);
assert.equal(runtimeIsVisibleInOnboarding("goose"), true);
assert.equal(runtimeIsVisibleInOnboarding("buzz-agent"), true);
assert.equal(runtimeIsVisibleInOnboarding("custom"), false);
});

Expand All @@ -30,7 +30,7 @@ test("visible onboarding runtimes use the product order", () => {

assert.deepEqual(
getVisibleOnboardingRuntimes(runtimes).map(({ id }) => id),
["claude", "codex"],
["claude", "codex", "goose", "buzz-agent"],
);
});

Expand All @@ -55,16 +55,17 @@ test("readiness requires an available and authenticated runtime", () => {
);
});

test("ready onboarding runtimes exclude hidden ready harnesses", () => {
test("ready onboarding runtimes exclude unknown and non-ready harnesses", () => {
const runtimes = [
runtime("goose", "available", "not_applicable"),
runtime("codex", "available", "logged_out"),
runtime("buzz-agent", "available", "not_applicable"),
runtime("claude", "available", "logged_in"),
runtime("custom", "available", "not_applicable"),
];

assert.deepEqual(
getReadyOnboardingRuntimes(runtimes).map(({ id }) => id),
["claude"],
["claude", "goose", "buzz-agent"],
);
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { AcpRuntimeCatalogEntry } from "@/shared/api/types";

export const ONBOARDING_RUNTIME_ORDER = ["claude", "codex"];
export const ONBOARDING_RUNTIME_ORDER = [
"claude",
"codex",
"goose",
"buzz-agent",
];

const VISIBLE_ONBOARDING_RUNTIME_IDS = new Set<string>(
ONBOARDING_RUNTIME_ORDER,
Expand Down
104 changes: 93 additions & 11 deletions desktop/tests/e2e/onboarding-agent-defaults.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ async function readSavedRuntime(page: Parameters<typeof installMockBridge>[0]) {
});
}

test("setup shows only Claude Code and Codex as detected harnesses", async ({
page,
}) => {
test("setup shows all bundled harnesses as detected", async ({ page }) => {
await installMockBridge(
page,
{
Expand All @@ -77,10 +75,8 @@ test("setup shows only Claude Code and Codex as detected harnesses", async ({

await expect(page.getByTestId("onboarding-runtime-claude")).toBeVisible();
await expect(page.getByTestId("onboarding-runtime-codex")).toBeVisible();
await expect(page.getByTestId("onboarding-runtime-goose")).toHaveCount(0);
await expect(page.getByTestId("onboarding-runtime-buzz-agent")).toHaveCount(
0,
);
await expect(page.getByTestId("onboarding-runtime-goose")).toBeVisible();
await expect(page.getByTestId("onboarding-runtime-buzz-agent")).toBeVisible();
await expect(page.getByRole("checkbox")).toHaveCount(0);
});

Expand Down Expand Up @@ -559,8 +555,8 @@ test("defaults auto-selects the only ready visible harness", async ({
page,
{
acpRuntimesCatalog: [
runtime("buzz-agent", "available", { status: "not_applicable" }),
runtime("goose", "available", { status: "not_applicable" }),
runtime("buzz-agent", "not_installed", { status: "not_applicable" }),
runtime("goose", "not_installed", { status: "not_applicable" }),
runtime("claude", "available", { status: "logged_in" }),
runtime("codex", "available", { status: "logged_out" }),
],
Expand Down Expand Up @@ -660,10 +656,10 @@ test("defaults requires a choice when multiple visible harnesses are ready", asy
).toBeVisible();
await expect(
page.getByTestId("global-agent-default-harness-option-goose"),
).toHaveCount(0);
).toBeVisible();
await expect(
page.getByTestId("global-agent-default-harness-option-buzz-agent"),
).toHaveCount(0);
).toBeVisible();
await page.getByTestId("global-agent-default-harness-option-codex").click();
await expect(harness).toHaveText("Codex");
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
Expand Down Expand Up @@ -836,3 +832,89 @@ test("concurrent installs each keep their own state — one fails, one succeeds"
});
expect(hasHorizontalOverflow).toBe(false);
});

test("Finish stays disabled until a provider-required harness is fully configured", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("buzz-agent", "available", { status: "not_applicable" }),
],
discoverAgentModels: {
models: [{ id: "claude-sonnet-4", name: "Claude Sonnet 4" }],
supportsSwitching: true,
},
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();

// buzz-agent auto-selects as the only ready harness, but with no provider
// configured the default is not launchable — Finish must be gated.
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Buzz",
);
const finish = page.getByTestId("onboarding-finish");
await expect(finish).toBeDisabled();

// Configure provider + credential; model resolves via discovery/fallback.
await page.getByTestId("global-agent-provider").click();
await page.getByTestId("global-agent-provider-option-anthropic").click();
await page.getByTestId("persona-provider-api-key").fill("sk-test-key");

await expect(finish).toBeEnabled();
await finish.click();
await expect(page.getByText("Join or create a community")).toBeVisible();
expect(await readSavedRuntime(page)).toBe("buzz-agent");
});

test("baked build config keeps Finish enabled without manual provider setup", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("buzz-agent", "available", { status: "not_applicable" }),
],
bakedBuildEnv: [
{ key: "BUZZ_AGENT_PROVIDER", masked: false, value: "databricks_v2" },
{
key: "DATABRICKS_HOST",
masked: false,
value: "https://example.cloud.databricks.com",
},
{ key: "DATABRICKS_MODEL", masked: false, value: "baked-model" },
],
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();

// Internal builds bake provider/model/credentials — the gate must treat
// baked config as complete and never block Finish.
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Buzz",
);
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
});
Loading