mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Restore Goose and Buzz Agent to onboarding harness selection (#2731)
> [!NOTE] > **Part 1 of a multi-PR onboarding rework.** This PR only restores the hidden harnesses, adjusts the card layout, and gates onboarding completion on valid config. A follow-up PR (stacked on this branch) restructures the flow: the harness page becomes a single-choice "pick your default harness" step, and install/sign-in/provider setup moves to the following page. Review this one on its own terms — the flow rework is intentionally not here. ## Summary - Restore **Goose** and **Buzz Agent** to onboarding harness selection, reverting the launch-only restriction from #2233 - The restore is the single centralized allowlist (`ONBOARDING_RUNTIME_ORDER` in `onboardingRuntimeSelection.ts`) that #2233 deliberately set up for this moment — setup cards, readiness handoff, and the defaults harness picker all derive from it - Lay the four harness cards out as a single row at `lg` and above (`lg:grid-cols-4`); below 1024px (including the app's 800px minimum window width) the grid is 2×2, and 1-up on narrow viewports - **Gate onboarding Finish on actual config validity** (review finding): the defaults page rendered provider/model/credential fields for provider-required harnesses but Finish only checked that a harness was selected — a fresh user picking Buzz Agent could persist a default that fails at first spawn. The Finish gate now consumes `AgentConfigFields`' existing `onValidityChange` signal. Baked build env and runtime-file config satisfy the gate, so internal builds and existing Goose users are never blocked - Update the unit + E2E specs that pinned the hidden behavior, plus two new E2E cases pinning the Finish gate (blocked-until-configured, and baked-env never blocked) - AGENTS.md rule 7 updated to document the completion gate ## Testing - `onboardingRuntimeSelection.test.mjs` — 4 passed - `pnpm typecheck` — clean - `pnpm exec playwright test tests/e2e/onboarding-agent-defaults.spec.ts --project=smoke` — 21 passed - `onboarding-docked-cta-screenshots.spec.ts` — 3 passed - Biome — clean ## Known cosmetic issue (deferred to PR 2) At the app's minimum window size (800×500) the 2×2 grid extends past the visible area and the footer CTA overlaps card space. Next still hit-tests correctly. PR 2 redesigns this page entirely (cards become a single-choice chooser with no inline setup), so this is deferred rather than patched twice. --------- Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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">
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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"],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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" }),
|
||||
],
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user