From e27923f2585c7a5afa0436488a61c69f9268861d Mon Sep 17 00:00:00 2001 From: kenny lopez Date: Fri, 17 Jul 2026 14:53:04 +0200 Subject: [PATCH 1/4] Improve agent runtime settings --- desktop/public/runtime-icons/claude.png | Bin 0 -> 8713 bytes desktop/public/runtime-icons/codex.png | Bin 0 -> 14676 bytes desktop/public/runtime-icons/goose.svg | 6 + desktop/src/app/AppShellContext.tsx | 2 +- desktop/src/app/routes/settings.tsx | 4 + .../agents/ui/AgentDefinitionDialog.tsx | 2 +- .../src/features/onboarding/ui/SetupStep.tsx | 4 +- .../src/features/profile/ui/ProfileAvatar.tsx | 4 +- .../settings/ui/DoctorSettingsPanel.tsx | 750 ++++++------------ .../features/settings/ui/SettingsPanels.tsx | 14 +- .../src/features/settings/ui/SettingsView.tsx | 9 +- .../src/shared/ui/config-nudge-attachment.tsx | 12 +- desktop/src/shared/ui/switch.tsx | 4 +- desktop/src/testing/e2eBridge.ts | 5 + .../tests/e2e/doctor-cta-screenshots.spec.ts | 12 +- desktop/tests/e2e/doctor-states.spec.ts | 293 +++++-- desktop/tests/e2e/profile.spec.ts | 6 +- desktop/tests/helpers/bridge.ts | 1 + desktop/tests/helpers/settings.ts | 3 +- 19 files changed, 536 insertions(+), 595 deletions(-) create mode 100644 desktop/public/runtime-icons/claude.png create mode 100644 desktop/public/runtime-icons/codex.png create mode 100644 desktop/public/runtime-icons/goose.svg diff --git a/desktop/public/runtime-icons/claude.png b/desktop/public/runtime-icons/claude.png new file mode 100644 index 0000000000000000000000000000000000000000..26f2160f5d31fc9051c852d6ac0996eba94e40b9 GIT binary patch literal 8713 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 literal 0 HcmV?d00001 diff --git a/desktop/public/runtime-icons/goose.svg b/desktop/public/runtime-icons/goose.svg new file mode 100644 index 0000000000..c5ed5e00a8 --- /dev/null +++ b/desktop/public/runtime-icons/goose.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/desktop/src/app/AppShellContext.tsx b/desktop/src/app/AppShellContext.tsx index 8da3bfc281..4a64de0cb2 100644 --- a/desktop/src/app/AppShellContext.tsx +++ b/desktop/src/app/AppShellContext.tsx @@ -49,7 +49,7 @@ type AppShellContextValue = { feedItemState: FeedItemState; // Open the Settings panel at the given section. Available on all surfaces // that render under AppShell (channel, home, projects, pulse, agents). - // Used by config-nudge cards to deep-link to Settings → Doctor. + // Used by config-nudge cards to deep-link to Settings → Agents. onOpenSettings: ((section: SettingsSection) => void) | null; }; diff --git a/desktop/src/app/routes/settings.tsx b/desktop/src/app/routes/settings.tsx index b3812ba511..0011018830 100644 --- a/desktop/src/app/routes/settings.tsx +++ b/desktop/src/app/routes/settings.tsx @@ -12,6 +12,10 @@ type SettingsRouteSearch = { function validateSettingsSearch( search: Record, ): SettingsRouteSearch { + if (search.section === "doctor") { + return { section: "agents" }; + } + return { section: isSettingsSection(search.section) ? search.section : undefined, }; diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx index ab4270b042..a0712b740c 100644 --- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx +++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx @@ -589,7 +589,7 @@ export function AgentDefinitionDialog({ : selectedRuntime.availability === "cli_missing" ? `${selectedRuntime.label} ACP adapter is installed but the CLI is missing.` : `${selectedRuntime.label} is not installed.`}{" "} - Visit Settings > Doctor to set it up. + Visit Settings > Agents to set it up.

) : null; const advancedFieldsTransition = shouldReduceMotion diff --git a/desktop/src/features/onboarding/ui/SetupStep.tsx b/desktop/src/features/onboarding/ui/SetupStep.tsx index e1eaf7203f..029672b80b 100644 --- a/desktop/src/features/onboarding/ui/SetupStep.tsx +++ b/desktop/src/features/onboarding/ui/SetupStep.tsx @@ -495,8 +495,8 @@ function RuntimeProvidersSection({ className="max-w-[560px] rounded-2xl bg-white/70 px-6 py-6 text-sm text-muted-foreground" data-testid="onboarding-acp-empty" > - No compatible ACP runtimes detected yet. You can finish setup now and - come back later in Settings > Doctor. + No compatible agent runtimes detected yet. You can finish setup now + and come back later in Settings > Agents.

)} diff --git a/desktop/src/features/profile/ui/ProfileAvatar.tsx b/desktop/src/features/profile/ui/ProfileAvatar.tsx index 899cf4ff9f..3a56ee329e 100644 --- a/desktop/src/features/profile/ui/ProfileAvatar.tsx +++ b/desktop/src/features/profile/ui/ProfileAvatar.tsx @@ -13,6 +13,7 @@ type ProfileAvatarProps = { label: string; className?: string; iconClassName?: string; + imageClassName?: string; plain?: boolean; testId?: string; }; @@ -23,6 +24,7 @@ export function ProfileAvatar({ label, className, iconClassName, + imageClassName, plain = false, testId, }: ProfileAvatarProps) { @@ -67,7 +69,7 @@ export function ProfileAvatar({ {src !== undefined ? ( { if (status === "error") setFailedSrc(liveSrc); diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx index baae562cd4..5cd5aee69c 100644 --- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx +++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx @@ -1,13 +1,5 @@ import * as React from "react"; -import { - AlertTriangle, - CheckCircle2, - Circle, - Download, - ExternalLink, - RefreshCw, - XCircle, -} from "lucide-react"; +import { EllipsisVertical, ExternalLink, RefreshCw } from "lucide-react"; import { openUrl } from "@tauri-apps/plugin-opener"; import { @@ -17,292 +9,214 @@ import { useGitBashPrerequisiteQuery, useInstallAcpRuntimeMutation, } from "@/features/agents/hooks"; -import { describeResolvedCommand } from "@/features/agents/ui/agentUi"; -import type { - AcpAuthMethod, - AcpRuntimeCatalogEntry, - AuthStatus, -} from "@/shared/api/types"; +import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; +import type { AcpAuthMethod, AcpRuntimeCatalogEntry } from "@/shared/api/types"; import { getInstallErrorMessage } from "@/shared/lib/installError"; import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; -import { SettingsOptionGroup } from "./SettingsOptionGroup"; -import { SettingsSectionHeader } from "./SettingsSectionHeader"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/shared/ui/dropdown-menu"; +import { SectionHeader } from "@/shared/ui/PageHeader"; +import { Spinner } from "@/shared/ui/spinner"; +import { Switch } from "@/shared/ui/switch"; -function StatusIcon({ - availability, -}: { - availability: AcpRuntimeCatalogEntry["availability"]; -}) { - switch (availability) { - case "available": - return ; - case "adapter_missing": - return ; - case "adapter_outdated": - return ; - case "cli_missing": - return ; - case "not_installed": - return ; - } -} +const RUNTIME_LOGO_URLS: Record = { + "buzz-agent": "/app-icon@2x.png", + claude: "/runtime-icons/claude.png", + codex: "/runtime-icons/codex.png", + goose: "/runtime-icons/goose.svg", +}; -function AuthStatusBadge({ authStatus }: { authStatus: AuthStatus }) { - switch (authStatus.status) { - case "logged_in": - return ( - - - Authenticated - - ); - case "logged_out": - return ( - - - Not authenticated - - ); - case "config_invalid": - return ( - - - Config error - - ); - case "not_applicable": - case "unknown": - return null; - } -} +const RUNTIME_LOGO_SCALE: Record = { + "buzz-agent": "scale-110", + claude: "scale-110", + codex: "scale-110", + goose: "scale-125", +}; -function AuthMethodButtonLabel({ method }: { method: AcpAuthMethod }) { - return <>{method.name || method.id}; +const RUNTIME_SORT_PRIORITY: Record = { + "buzz-agent": 0, + goose: 1, +}; + +function RuntimeLogo({ runtime }: { runtime: AcpRuntimeCatalogEntry }) { + const avatarUrl = RUNTIME_LOGO_URLS[runtime.id] ?? runtime.avatarUrl; + + return ( + + ); } -function ConnectAccountActions({ +function RuntimeOverflowMenu({ + connectingMethodId, + isConnecting, + onConnect, runtime, }: { + connectingMethodId: string | null; + isConnecting: boolean; + onConnect: (method: AcpAuthMethod) => void; runtime: AcpRuntimeCatalogEntry; }) { + const canConnectAccount = + runtime.availability === "available" && + runtime.authStatus.status === "logged_out"; const authMethodsQuery = useAcpAuthMethodsQuery(runtime.id, { - enabled: - runtime.availability === "available" && - runtime.authStatus.status === "logged_out", + enabled: canConnectAccount, }); - const connectMutation = useConnectAcpRuntimeMutation(); - const [terminalLaunchMethodId, setTerminalLaunchMethodId] = React.useState< - string | null - >(null); + const methods = canConnectAccount + ? (authMethodsQuery.data?.methods ?? []) + : []; + const hasInstructions = + runtime.installInstructionsUrl.trim().length > 0 && + (runtime.availability !== "available" || + runtime.authStatus.status === "logged_out" || + runtime.authStatus.status === "config_invalid"); + const hasActions = + runtime.nodeRequired || hasInstructions || methods.length > 0; - if (runtime.authStatus.status !== "logged_out") { + if (!hasActions) { return null; } - const methods = authMethodsQuery.data?.methods ?? []; - const isConnecting = connectMutation.isPending; - - function connect(method: AcpAuthMethod) { - connectMutation.mutate( - { runtimeId: runtime.id, methodId: method.id }, - { - onSuccess: (result) => { - if (result.launched && method.type === "terminal") { - setTerminalLaunchMethodId(method.id); - } - }, - }, - ); - } - - if (authMethodsQuery.isLoading) { - return ( -

- Looking for account connection options... -

- ); - } - - if (authMethodsQuery.error instanceof Error) { - return ( -

- Couldn't load account connection options:{" "} - {authMethodsQuery.error.message} -

- ); - } - - if (methods.length === 0) { - return ( -

- This adapter did not advertise a built-in login flow. Use the manual - instructions above, then click Re-run. -

- ); - } - return ( -
-
- {methods.map((method) => { - const pending = - isConnecting && connectMutation.variables?.methodId === method.id; - return ( - - ); - })} -
-

- Buzz launches the adapter's own login flow and then re-checks the{" "} - {runtime.label} CLI. Credentials stay with {runtime.label}. -

- {terminalLaunchMethodId ? ( -

- Finish signing in from the Terminal window, then click Re-run to - re-check {runtime.label}. -

- ) : null} - {methods.map((method) => - method.description ? ( -

- - {method.name || method.id}: - {" "} - {method.description} -

- ) : null, - )} - {connectMutation.error instanceof Error ? ( -

- {connectMutation.error.message} -

- ) : null} -
+ + + + + event.preventDefault()} + > + {methods.map((method) => ( + onConnect(method)} + > + {isConnecting && connectingMethodId === method.id ? ( + + ) : null} + {method.name || method.id} + + ))} + {runtime.nodeRequired ? ( + void openUrl("https://nodejs.org")}> + + Install Node.js + + ) : null} + {hasInstructions ? ( + void openUrl(runtime.installInstructionsUrl)} + > + + Instructions + + ) : null} + + ); } -function InstallActions({ - hasError, +function RuntimeActions({ + installSuccess, isInstalling, onInstall, runtime, }: { - hasError: boolean; + installSuccess: boolean; isInstalling: boolean; onInstall: () => void; runtime: AcpRuntimeCatalogEntry; }) { - const showInstall = runtime.canAutoInstall && !runtime.nodeRequired; - const installLabel = - runtime.availability === "adapter_missing" - ? "Install ACP adapter" - : runtime.availability === "adapter_outdated" - ? "Update ACP adapter" - : `Install ${runtime.label}`; - const pendingLabel = - runtime.availability === "adapter_missing" || - runtime.availability === "adapter_outdated" - ? "Installing adapter..." - : `Installing ${runtime.label}...`; + const isAvailable = runtime.availability === "available"; + const canInstall = runtime.canAutoInstall && !runtime.nodeRequired; + const isOn = isAvailable || installSuccess; + const connectMutation = useConnectAcpRuntimeMutation(); + const isConnecting = connectMutation.isPending; + const isWorking = isInstalling || isConnecting; return ( -
- {showInstall ? ( -

- Buzz uses the official installer and adds the ACP adapter. After it - finishes, Buzz will show the vendor's sign-in flow here. -

- ) : null} -
- {showInstall ? ( - - ) : null} - -
+
+ + connectMutation.mutate({ + runtimeId: runtime.id, + methodId: method.id, + }) + } + runtime={runtime} + /> + {isWorking ? ( +
+ +
+ ) : ( + { + if (checked) { + onInstall(); + } + }} + /> + )}
); } -/** - * Node.js callout when required, or the install actions when it is not. - * Used for both `adapter_missing` and `not_installed` availability states. - * The `cli_missing` branch is intentionally excluded — its install path does - * not involve npm, so no Node.js gate applies. - */ -function NodeRequiredOrInstall({ - hasError, +function RuntimeHeader({ + installSuccess, isInstalling, onInstall, runtime, }: { - hasError: boolean; + installSuccess: boolean; isInstalling: boolean; onInstall: () => void; runtime: AcpRuntimeCatalogEntry; }) { - if (runtime.nodeRequired) { - return ( -

- Node.js is required to install this adapter.{" "} - - , then click Re-run. -

- ); - } return ( - +
+
+ +

{runtime.label}

+
+ +
); } @@ -321,179 +235,16 @@ function RuntimeRow({ }) { return (
-
- -
- -
-
-

{runtime.label}

- {runtime.command ? ( - - {runtime.command} - - ) : null} -
- - {runtime.availability === "available" && - runtime.command && - runtime.binaryPath ? ( - <> -

- Available via{" "} - {describeResolvedCommand(runtime.command, runtime.binaryPath)}. -

- {runtime.defaultArgs.length > 0 ? ( -

- Default args:{" "} - - {runtime.defaultArgs.join(", ")} - -

- ) : null} - {runtime.underlyingCliPath && - runtime.underlyingCliPath !== runtime.binaryPath ? ( -
-

- CLI:{" "} - {runtime.underlyingCliPath} -

-

- ACP adapter:{" "} - {runtime.binaryPath} -

-
- ) : ( - <> -

- {runtime.binaryPath} -

-

- ACP support built-in — no separate adapter needed. -

- - )} - {/* - * Auth badge renders only for `available` runtimes: non-available - * entries always have auth_status: unknown (no probe was run), which - * AuthStatusBadge maps to null. Rendering it here is self-consistent. - */} - {runtime.authStatus.status !== "not_applicable" && - runtime.authStatus.status !== "unknown" ? ( -
- -
- ) : null} - {/* Login hint shown when not logged in or the config is invalid */} - {runtime.loginHint && - runtime.authStatus.status !== "not_applicable" && - runtime.authStatus.status !== "unknown" ? ( -

- {runtime.authStatus.status === "config_invalid" - ? `Config error: ${runtime.authStatus.diagnostic}` - : runtime.loginHint} -

- ) : null} - - - ) : runtime.availability === "adapter_missing" ? ( - <> -

- CLI detected at{" "} - - {runtime.underlyingCliPath ?? "unknown path"} - {" "} - but ACP adapter not found. -

-

- {runtime.installHint} -

- - - ) : runtime.availability === "adapter_outdated" ? ( - <> -

- ACP adapter found at{" "} - - {runtime.binaryPath ?? "unknown path"} - {" "} - but it is from the deprecated package. Reinstall to enable relay - connectivity. -

-

- This updates the machine-global{" "} - - codex-acp - {" "} - adapter. Older Buzz releases using the legacy adapter contract may - lose community access until{" "} - - @zed-industries/codex-acp@0.16.0 - {" "} - is restored. -

-

- {runtime.installHint} -

- - - ) : runtime.availability === "cli_missing" ? ( - <> -

- ACP adapter found at{" "} - - {runtime.binaryPath ?? "unknown path"} - {" "} - but the {runtime.label} CLI is not installed. -

-

- {runtime.installHint} -

- - - ) : ( - <> -

- Not installed -

-

- {runtime.installHint} -

- - - )} +
+ {installSuccess && runtime.availability !== "available" ? (

@@ -510,7 +261,7 @@ function RuntimeRow({ ); } -function GitBashRow({ +function GitBashCard({ prerequisite, }: { prerequisite: NonNullable< @@ -519,39 +270,47 @@ function GitBashRow({ }) { return (

-
- {prerequisite.available ? ( - - ) : ( - - )} -
-
-

Git Bash

- {prerequisite.available ? ( -

- {prerequisite.path} -

- ) : ( - <> -

- Required for buzz-agent shell tools on Windows. -

-

- {prerequisite.installHint} -

+
+
+
+

Git Bash

+ + + {prerequisite.available ? "Available" : "Action needed"} + +
+ {!prerequisite.available ? ( - - )} + ) : null} +
+ {!prerequisite.available ? ( +

+ Required for buzz-agent shell tools on Windows. +

+ ) : null}
); @@ -560,7 +319,15 @@ function GitBashRow({ export function DoctorSettingsPanel() { const runtimesQuery = useAcpRuntimesQuery(); const gitBashQuery = useGitBashPrerequisiteQuery(); - const runtimes = runtimesQuery.data ?? []; + const runtimes = React.useMemo( + () => + [...(runtimesQuery.data ?? [])].sort( + (left, right) => + (RUNTIME_SORT_PRIORITY[left.id] ?? Number.MAX_SAFE_INTEGER) - + (RUNTIME_SORT_PRIORITY[right.id] ?? Number.MAX_SAFE_INTEGER), + ), + [runtimesQuery.data], + ); const isRefreshing = runtimesQuery.isFetching; const installMutation = useInstallAcpRuntimeMutation(); const [installResults, setInstallResults] = React.useState< @@ -617,10 +384,14 @@ export function DoctorSettingsPanel() { } return ( -
- + - Re-run + Check again } /> -
- - {gitBashQuery.data ? ( - <> -
-

- System prerequisites -

-

- Windows tools required by supported agents. -

-
- - - ) : null} -
-

- Agent CLIs and ACP runtimes -

-

- Installation status of supported agent CLIs and their ACP - runtimes. -

-
+
+ {gitBashQuery.data ? ( +
+
+

+ System prerequisites +

+

+ Windows tools required by supported agents. +

+
+ +
+ ) : null} +
{runtimesQuery.isLoading ? ( -
- Looking for ACP runtimes... +
+ Checking agent runtimes...
) : runtimes.length > 0 ? ( - runtimes.map((runtime) => ( - handleInstall(runtime.id)} - runtime={runtime} - /> - )) +
+ {runtimes.map((runtime) => ( + handleInstall(runtime.id)} + runtime={runtime} + /> + ))} +
) : ( -
- No known ACP runtimes found. +
+ No supported agent runtimes found.
)} {runtimesQuery.error instanceof Error ? ( -

+

{runtimesQuery.error.message}

) : null} - +
); diff --git a/desktop/src/features/settings/ui/SettingsPanels.tsx b/desktop/src/features/settings/ui/SettingsPanels.tsx index 767d758946..caf027eab9 100644 --- a/desktop/src/features/settings/ui/SettingsPanels.tsx +++ b/desktop/src/features/settings/ui/SettingsPanels.tsx @@ -16,7 +16,6 @@ import { ShieldAlert, Smartphone, Smile, - Stethoscope, Sun, SunMoon, UserRound, @@ -82,8 +81,7 @@ export type SettingsSection = | "custom-emoji" | "local-archive" | "mobile" - | "updates" - | "doctor"; + | "updates"; export const DEFAULT_SETTINGS_SECTION: SettingsSection = "profile"; @@ -102,7 +100,6 @@ const SETTINGS_SECTION_VALUES: readonly SettingsSection[] = [ "local-archive", "mobile", "updates", - "doctor", ]; export function isSettingsSection(value: unknown): value is SettingsSection { @@ -209,12 +206,6 @@ export const settingsSections: SettingsSectionDescriptor[] = [ label: "Updates", icon: Download, }, - { - value: "doctor", - label: "Doctor", - icon: Stethoscope, - featureGate: "doctor", - }, ]; function formatThemeLabel(name: string): string { @@ -716,6 +707,7 @@ export function renderSettingsSection( return (
+
); @@ -741,8 +733,6 @@ export function renderSettingsSection( return ; case "updates": return ; - case "doctor": - return ; default: { const exhaustiveCheck: never = section; return exhaustiveCheck; diff --git a/desktop/src/features/settings/ui/SettingsView.tsx b/desktop/src/features/settings/ui/SettingsView.tsx index bf6b6e8682..56ee0eed00 100644 --- a/desktop/src/features/settings/ui/SettingsView.tsx +++ b/desktop/src/features/settings/ui/SettingsView.tsx @@ -65,14 +65,7 @@ const settingsNavGroups: Array<{ }, { label: "App", - sections: [ - "agents", - "compute", - "experimental", - "mobile", - "updates", - "doctor", - ], + sections: ["agents", "compute", "experimental", "mobile", "updates"], }, ]; diff --git a/desktop/src/shared/ui/config-nudge-attachment.tsx b/desktop/src/shared/ui/config-nudge-attachment.tsx index 4be3e63980..321feab912 100644 --- a/desktop/src/shared/ui/config-nudge-attachment.tsx +++ b/desktop/src/shared/ui/config-nudge-attachment.tsx @@ -165,7 +165,7 @@ export function focusTargetForRequirement( * Routing: * (A) Any card with a `git_bash` requirement, or one whose requirements are all * install-state `cli_login`, opens Settings → Doctor. A card-level - * "Open Doctor →" label in `AttachmentActions` confirms the action at rest. + * Agent runtimes label in `AttachmentActions` confirms the action at rest. * (A-auth) A card whose requirements are all available `cli_login` surfaces is * purely informational: Doctor cannot authenticate a CLI, and `setup_copy` * already gives the needed command. @@ -199,7 +199,7 @@ export function ConfigNudgeCard({ "[ConfigNudgeCard] onOpenSettings is null — Doctor deep-link unavailable on this surface", ); } - onOpenSettings?.("doctor"); + onOpenSettings?.("agents"); }; const openEditAgent = (focus?: EditAgentFocusTarget) => { @@ -270,7 +270,9 @@ export function ConfigNudgeCard({ cards have none; other mixed cards render their own row CTAs. */} {opensDoctor && !informationalOnly && ( - Open Doctor → + + Open Agent runtimes → + )} {/* Informational-only cards are purely informational — no trigger, no routing. */} @@ -278,7 +280,7 @@ export function ConfigNudgeCard({ - Open Doctor → + Open Agent runtimes → )}
diff --git a/desktop/src/shared/ui/switch.tsx b/desktop/src/shared/ui/switch.tsx index 420fee2d34..c22ae90316 100644 --- a/desktop/src/shared/ui/switch.tsx +++ b/desktop/src/shared/ui/switch.tsx @@ -9,7 +9,7 @@ const Switch = React.forwardRef< >(({ className, ...props }, ref) => ( diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index f9ae719999..c6449b7d64 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -127,6 +127,7 @@ type E2eConfig = { connectAcpRuntimeDelayMs?: number; connectAcpRuntimeError?: string; activePersonaIds?: string[]; + installAcpRuntimeDelayMs?: number; installAcpRuntimeResult?: RawInstallRuntimeResult; /** Sequence of results for successive `install_acp_runtime` calls. * Call N returns results[N]; when exhausted the last entry repeats. @@ -6644,6 +6645,10 @@ async function handleInstallAcpRuntime( }, config: E2eConfig | undefined, ): Promise { + const delayMs = config?.mock?.installAcpRuntimeDelayMs ?? 0; + if (delayMs > 0) { + await new Promise((resolve) => window.setTimeout(resolve, delayMs)); + } const sequence = config?.mock?.installAcpRuntimeResults; if (sequence && sequence.length > 0) { const idx = Math.min(installCallCount, sequence.length - 1); diff --git a/desktop/tests/e2e/doctor-cta-screenshots.spec.ts b/desktop/tests/e2e/doctor-cta-screenshots.spec.ts index 9bd2732175..cde18b7ff0 100644 --- a/desktop/tests/e2e/doctor-cta-screenshots.spec.ts +++ b/desktop/tests/e2e/doctor-cta-screenshots.spec.ts @@ -155,8 +155,8 @@ test.describe("doctor CTA nudge card screenshots", () => { // Wait for the nudge card to render. const card = page.locator("[data-config-nudge]").last(); await expect(card).toBeVisible({ timeout: 10_000 }); - // Auth-only card is informational — no Doctor CTA anywhere. - await expect(card.getByText("Open Doctor →")).toHaveCount(0); + // Auth-only card is informational — no runtime settings CTA anywhere. + await expect(card.getByText("Open Agent runtimes →")).toHaveCount(0); await card.scrollIntoViewIfNeeded(); await settleAnimations(page); @@ -168,7 +168,7 @@ test.describe("doctor CTA nudge card screenshots", () => { /** * 02 — not_installed state: neither adapter nor CLI found. - * Card shows "claude isn't installed" copy + inline "Open Doctor →" CTA. + * Card shows "claude isn't installed" copy + an Agent runtimes CTA. */ test("02-cli-login-not-installed-state", async ({ page }) => { await installMockBridge(page, { @@ -210,7 +210,7 @@ test.describe("doctor CTA nudge card screenshots", () => { /** * 03 — mixed card: one cli_login (adapter_missing) + one env_key requirement. * Each requirement row owns its CTA, right-aligned to a shared edge: - * the cli_login row shows "Open Doctor →" and the env_key row shows + * the cli_login row opens Agent runtimes and the env_key row shows * "Edit Agent →", both at the same x (vertically aligned). */ test("03-mixed-requirements-inline-doctor-cta", async ({ page }) => { @@ -244,8 +244,8 @@ test.describe("doctor CTA nudge card screenshots", () => { const card = page.locator("[data-config-nudge]").last(); await expect(card).toBeVisible({ timeout: 10_000 }); - // Mixed card: cli_login row shows "Open Doctor →", env_key row shows "Edit Agent →". - await expect(card.getByText("Open Doctor →")).toBeVisible(); + // Mixed card: cli_login opens Agent runtimes; env_key opens Edit Agent. + await expect(card.getByText("Open Agent runtimes →")).toBeVisible(); // Both per-row CTAs share the same right edge (vertically aligned). await expect(card.getByText("Edit Agent →", { exact: true })).toBeVisible(); diff --git a/desktop/tests/e2e/doctor-states.spec.ts b/desktop/tests/e2e/doctor-states.spec.ts index 6675ef23b3..077dff4fbe 100644 --- a/desktop/tests/e2e/doctor-states.spec.ts +++ b/desktop/tests/e2e/doctor-states.spec.ts @@ -15,7 +15,7 @@ const SHOTS = "test-results/screenshots-doctor"; const GOOSE_AVAILABLE = { id: "goose", label: "Goose", - avatar_url: "", + avatar_url: "https://goose-docs.ai/img/logo_dark.png", availability: "available", command: "goose", binary_path: "/usr/local/bin/goose", @@ -54,7 +54,8 @@ const BUZZ_AGENT_AVAILABLE = { const CLAUDE_AVAILABLE_LOGGED_IN = { id: "claude", label: "Claude Code", - avatar_url: "", + avatar_url: + "https://anthropic.gallerycdn.vsassets.io/extensions/anthropic/claude-code/2.1.77/1773707456892/Microsoft.VisualStudio.Services.Icons.Default", availability: "available", command: "claude-agent-acp", binary_path: "/usr/local/bin/claude-agent-acp", @@ -76,7 +77,8 @@ const CLAUDE_AVAILABLE_LOGGED_IN = { const CODEX_NOT_INSTALLED = { id: "codex", label: "Codex", - avatar_url: "", + avatar_url: + "https://openai.gallerycdn.vsassets.io/extensions/openai/chatgpt/26.5313.41514/1773706730621/Microsoft.VisualStudio.Services.Icons.Default", availability: "not_installed", command: null, binary_path: null, @@ -111,9 +113,116 @@ test.describe("Doctor panel state screenshots", () => { }); /** - * 01 — available runtime that passed the auth probe: green "Authenticated" - * badge appears below the binary path. + * 00 — the runtime catalog reads as a set of individual status cards rather + * than one continuous table. */ + test("00-runtime-card-layout", async ({ page }) => { + await installMockBridge(page, { + acpRuntimesCatalog: [ + GOOSE_AVAILABLE, + CLAUDE_AVAILABLE_LOGGED_IN, + CODEX_NOT_INSTALLED, + BUZZ_AGENT_AVAILABLE, + ], + }); + + await page.goto("/", { waitUntil: "domcontentloaded" }); + await openSettings(page, "agents"); + + const runtimeList = page.getByTestId("doctor-runtime-list"); + await expect(runtimeList).toBeVisible({ timeout: 10_000 }); + await expect(page.getByTestId("doctor-runtime-goose")).toBeVisible(); + await expect(page.getByTestId("doctor-runtime-codex")).toBeVisible(); + await expect( + runtimeList.locator(":scope > [data-testid^='doctor-runtime-']"), + ).toHaveCount(4); + expect( + await runtimeList + .locator(":scope > [data-testid^='doctor-runtime-']") + .evaluateAll((rows) => + rows.map((row) => row.getAttribute("data-testid")), + ), + ).toEqual([ + "doctor-runtime-buzz-agent", + "doctor-runtime-goose", + "doctor-runtime-claude", + "doctor-runtime-codex", + ]); + for (const runtimeId of ["goose", "claude", "codex", "buzz-agent"]) { + await expect( + page.getByTestId(`doctor-runtime-logo-${runtimeId}`), + ).toBeVisible(); + } + const rowHeights = await Promise.all( + ["goose", "claude", "codex", "buzz-agent"].map((runtimeId) => + page + .getByTestId(`doctor-runtime-${runtimeId}`) + .evaluate((element) => + Math.round(element.getBoundingClientRect().height), + ), + ), + ); + expect(new Set(rowHeights).size).toBe(1); + const [gooseColors, codexColors] = await Promise.all( + ["goose", "codex"].map((runtimeId) => + page.getByTestId(`doctor-runtime-${runtimeId}`).evaluate((element) => { + const styles = getComputedStyle(element); + return { + backgroundColor: styles.backgroundColor, + borderColor: styles.borderColor, + }; + }), + ), + ); + expect(codexColors).toEqual(gooseColors); + await expect( + page + .getByRole("heading", { name: "Agent runtimes" }) + .locator("..") + .locator(".."), + ).toHaveCSS("align-items", "center"); + for (const runtimeId of ["goose", "claude", "buzz-agent"]) { + await expect( + page.getByTestId(`doctor-runtime-menu-${runtimeId}`), + ).toHaveCount(0); + } + await expect( + page.getByTestId("doctor-runtime-toggle-codex"), + ).not.toBeChecked(); + await expect(page.getByTestId("doctor-runtime-toggle-codex")).toBeEnabled(); + for (const runtimeId of ["goose", "codex"]) { + const toggle = page.getByTestId(`doctor-runtime-toggle-${runtimeId}`); + await expect(toggle).toHaveClass(/shadow-none/); + await expect(toggle.locator("span")).toHaveClass(/shadow-none/); + } + await expect( + page.getByRole("menuitem", { name: "Instructions" }), + ).toHaveCount(0); + await page.getByTestId("doctor-runtime-menu-codex").click(); + await expect( + page.getByRole("menuitem", { name: "Instructions" }), + ).toBeVisible(); + await waitForAnimations(page); + await page.screenshot({ + path: `${SHOTS}/00-runtime-overflow-menu.png`, + }); + await page.keyboard.press("Escape"); + await expect(page.getByTestId("doctor-runtime-toggle-goose")).toBeChecked(); + await expect( + page.getByTestId("doctor-runtime-toggle-goose"), + ).toBeDisabled(); + await expect(page.getByTestId("doctor-runtime-codex")).not.toContainText( + "Not installed", + ); + + await runtimeList.scrollIntoViewIfNeeded(); + await waitForAnimations(page); + await runtimeList.screenshot({ + path: `${SHOTS}/00-runtime-card-layout.png`, + }); + }); + + /** 01 — a ready runtime stays compact without redundant status copy. */ test("01-auth-logged-in", async ({ page }) => { await installMockBridge(page, { acpRuntimesCatalog: [ @@ -125,11 +234,18 @@ test.describe("Doctor panel state screenshots", () => { }); await page.goto("/", { waitUntil: "domcontentloaded" }); - await openSettings(page, "doctor"); + await openSettings(page, "agents"); const row = page.getByTestId("doctor-runtime-claude"); await expect(row).toBeVisible({ timeout: 10_000 }); - await expect(row).toContainText("Authenticated"); + await expect( + page.getByTestId("doctor-runtime-toggle-claude"), + ).toBeChecked(); + await expect(row).not.toContainText("Authenticated"); + await expect(row).not.toContainText("Available"); + await expect(row).not.toContainText("claude-agent-acp"); + await expect(row).not.toContainText("/usr/local/bin"); + await expect(page.getByTestId("doctor-runtime-menu-claude")).toHaveCount(0); await row.scrollIntoViewIfNeeded(); await waitForAnimations(page); @@ -137,8 +253,8 @@ test.describe("Doctor panel state screenshots", () => { }); /** - * 02 — available runtime that failed the auth probe: amber "Not - * authenticated" badge + login hint shown below the binary path. + * 02 — an available runtime that needs authentication stays the same height + * as the others and moves setup instructions into its overflow menu. */ test("02-auth-logged-out", async ({ page }) => { await installMockBridge(page, { @@ -159,12 +275,23 @@ test.describe("Doctor panel state screenshots", () => { }); await page.goto("/", { waitUntil: "domcontentloaded" }); - await openSettings(page, "doctor"); + await openSettings(page, "agents"); const row = page.getByTestId("doctor-runtime-codex"); await expect(row).toBeVisible({ timeout: 10_000 }); - await expect(row).toContainText("Not authenticated"); - await expect(row).toContainText("Run `codex login` to authenticate."); + await expect(row).not.toContainText("Not authenticated"); + await expect(row).not.toContainText("Run `codex login` to authenticate."); + await expect(row).toHaveCSS( + "height", + await page + .getByTestId("doctor-runtime-goose") + .evaluate((element) => getComputedStyle(element).height), + ); + await page.getByTestId("doctor-runtime-menu-codex").click(); + await expect( + page.getByRole("menuitem", { name: "Instructions" }), + ).toBeVisible(); + await page.keyboard.press("Escape"); await row.scrollIntoViewIfNeeded(); await waitForAnimations(page); @@ -172,8 +299,8 @@ test.describe("Doctor panel state screenshots", () => { }); /** - * 03 — available runtime whose CLI has a config-parse error: red "Config - * error" badge + diagnostic excerpt shown below the binary path. + * 03 — a runtime with invalid configuration remains compact and exposes its + * setup instructions from overflow instead of expanding the row. */ test("03-auth-config-error", async ({ page }) => { const diagnostic = @@ -192,12 +319,23 @@ test.describe("Doctor panel state screenshots", () => { }); await page.goto("/", { waitUntil: "domcontentloaded" }); - await openSettings(page, "doctor"); + await openSettings(page, "agents"); const row = page.getByTestId("doctor-runtime-claude"); await expect(row).toBeVisible({ timeout: 10_000 }); - await expect(row).toContainText("Config error"); - await expect(row).toContainText("error loading configuration"); + await expect(row).not.toContainText("Config error"); + await expect(row).not.toContainText("error loading configuration"); + await expect(row).toHaveCSS( + "height", + await page + .getByTestId("doctor-runtime-goose") + .evaluate((element) => getComputedStyle(element).height), + ); + await page.getByTestId("doctor-runtime-menu-claude").click(); + await expect( + page.getByRole("menuitem", { name: "Instructions" }), + ).toBeVisible(); + await page.keyboard.press("Escape"); await row.scrollIntoViewIfNeeded(); await waitForAnimations(page); @@ -205,9 +343,8 @@ test.describe("Doctor panel state screenshots", () => { }); /** - * 04 — adapter_missing runtime with node_required: true: the amber "Node.js - * is required…" callout replaces the Install button so the user cannot - * inadvertently trigger a doomed npm install. + * 04 — adapter_missing runtime with node_required: true: the off toggle is + * disabled, and the Node.js action moves into the overflow menu. */ test("04-node-required", async ({ page }) => { await installMockBridge(page, { @@ -227,15 +364,26 @@ test.describe("Doctor panel state screenshots", () => { }); await page.goto("/", { waitUntil: "domcontentloaded" }); - await openSettings(page, "doctor"); + await openSettings(page, "agents"); const row = page.getByTestId("doctor-runtime-codex"); await expect(row).toBeVisible({ timeout: 10_000 }); - await expect(row).toContainText("Node.js is required"); - // Exact-name match so "Install Node.js" (inside the callout) is not counted. + const toggle = page.getByTestId("doctor-runtime-toggle-codex"); + await expect(toggle).not.toBeChecked(); + await expect(toggle).toBeDisabled(); + await expect(row).not.toContainText("Adapter needed"); + await expect(row).not.toContainText("Node.js is required"); + await expect(row).toHaveCSS( + "height", + await page + .getByTestId("doctor-runtime-goose") + .evaluate((element) => getComputedStyle(element).height), + ); + await page.getByTestId("doctor-runtime-menu-codex").click(); await expect( - row.getByRole("button", { name: "Install", exact: true }), - ).toHaveCount(0); + page.getByRole("menuitem", { name: "Install Node.js" }), + ).toBeVisible(); + await page.keyboard.press("Escape"); await row.scrollIntoViewIfNeeded(); await waitForAnimations(page); @@ -243,13 +391,13 @@ test.describe("Doctor panel state screenshots", () => { }); /** - * 05 — a failed install renders a "Retry" button; clicking Retry succeeds. + * 05 — a failed toggle install returns to off; toggling again retries. * * The mock is configured with a two-call sequence: * call 1 → failure (E404) * call 2 → success - * This exercises the full Retry UX path: fail state → click Retry → - * spinner → success banner. + * This exercises the full retry path: fail state → toggle on again → + * success banner. */ test("05-retry-after-failure", async ({ page }) => { await installMockBridge(page, { @@ -263,6 +411,7 @@ test.describe("Doctor panel state screenshots", () => { }, BUZZ_AGENT_AVAILABLE, ], + installAcpRuntimeDelayMs: 250, installAcpRuntimeResults: [ { success: false, @@ -294,19 +443,25 @@ test.describe("Doctor panel state screenshots", () => { }); await page.goto("/", { waitUntil: "domcontentloaded" }); - await openSettings(page, "doctor"); + await openSettings(page, "agents"); const row = page.getByTestId("doctor-runtime-codex"); await expect(row).toBeVisible({ timeout: 10_000 }); + await expect(row).not.toContainText("Not installed"); // Trigger the first install — the mock returns a failure. - const installBtn = row.getByRole("button", { name: "Install Codex" }); - await expect(installBtn).toBeVisible({ timeout: 5_000 }); - await installBtn.click(); - - // After failure: Retry button appears and the error message is visible. - const retryBtn = row.getByRole("button", { name: "Retry Install Codex" }); - await expect(retryBtn).toBeVisible({ timeout: 5_000 }); + const toggle = page.getByTestId("doctor-runtime-toggle-codex"); + await expect(toggle).not.toBeChecked(); + await expect(toggle).toBeEnabled(); + await toggle.click(); + const loading = page.getByTestId("doctor-runtime-loading-codex"); + await expect(loading).toBeVisible(); + await expect(toggle).toHaveCount(0); + + // After failure: the toggle returns to off and the error is visible. + await expect(loading).toHaveCount(0, { timeout: 5_000 }); + await expect(toggle).not.toBeChecked({ timeout: 5_000 }); + await expect(toggle).toBeEnabled(); await expect(row).toContainText("Step"); await expect(row).toContainText("failed"); @@ -314,17 +469,21 @@ test.describe("Doctor panel state screenshots", () => { await waitForAnimations(page); await row.screenshot({ path: `${SHOTS}/05-retry-after-failure.png` }); - // Click Retry — the mock returns success on the second call. - await retryBtn.click(); + // Toggle on again — the mock returns success on the second call. + await toggle.click(); + await expect(loading).toBeVisible(); + await expect(toggle).toHaveCount(0); - // Error paragraph must disappear and per-runtime spinner must appear, - // then the success banner must render. + // The error disappears, then the success banner and on state render. + await expect(loading).toHaveCount(0, { timeout: 5_000 }); await expect(row).not.toContainText("failed", { timeout: 5_000 }); await expect( row.getByText("Codex installed. Checking for sign-in options..."), ).toBeVisible({ timeout: 10_000, }); + await expect(toggle).toBeChecked(); + await expect(toggle).toBeDisabled(); await row.scrollIntoViewIfNeeded(); await waitForAnimations(page); @@ -332,9 +491,8 @@ test.describe("Doctor panel state screenshots", () => { }); /** - * 06 — logged-out runtime with adapter-advertised auth methods: Doctor shows - * adapter-provided labels/descriptions and clicking one launches the - * vendor-owned flow through the mocked connect command. + * 06 — adapter-provided account methods appear in the overflow menu and + * launch the vendor-owned flow without expanding the runtime row. */ test("06-connect-account-methods", async ({ page }) => { await installMockBridge(page, { @@ -368,28 +526,36 @@ test.describe("Doctor panel state screenshots", () => { }); await page.goto("/", { waitUntil: "domcontentloaded" }); - await openSettings(page, "doctor"); + await openSettings(page, "agents"); const row = page.getByTestId("doctor-runtime-codex"); await expect(row).toBeVisible({ timeout: 10_000 }); - await expect(row).toContainText("Not authenticated"); - await expect(row).toContainText("Sign in with ChatGPT"); - await expect(row).toContainText( - "Use your Codex subscription in the browser.", + await expect(row).not.toContainText("Not authenticated"); + await expect(row).toHaveCSS( + "height", + await page + .getByTestId("doctor-runtime-goose") + .evaluate((element) => getComputedStyle(element).height), ); - await expect(row).toContainText("Credentials stay with Codex."); - - await row.getByRole("button", { name: "Sign in with ChatGPT" }).click(); + await page.getByTestId("doctor-runtime-menu-codex").click(); await expect( - row.getByRole("button", { name: "Connecting..." }), + page.getByRole("menuitem", { name: "Sign in with ChatGPT" }), ).toBeVisible({ timeout: 5_000, }); + await page.getByRole("menuitem", { name: "Sign in with ChatGPT" }).click(); + const loading = page.getByTestId("doctor-runtime-loading-codex"); + await expect(loading).toBeVisible(); + await expect(page.getByTestId("doctor-runtime-toggle-codex")).toHaveCount( + 0, + ); + await expect(loading).toHaveCount(0, { timeout: 5_000 }); + await expect(page.getByTestId("doctor-runtime-toggle-codex")).toBeChecked(); }); /** - * 07 — old or constrained adapter with no advertised auth methods: Doctor - * falls back to manual instructions instead of inventing a login command. + * 07 — an adapter with no advertised auth methods shows only its manual + * instructions in overflow and keeps the row compact. */ test("07-connect-account-no-methods", async ({ page }) => { await installMockBridge(page, { @@ -409,14 +575,23 @@ test.describe("Doctor panel state screenshots", () => { }); await page.goto("/", { waitUntil: "domcontentloaded" }); - await openSettings(page, "doctor"); + await openSettings(page, "agents"); const row = page.getByTestId("doctor-runtime-claude"); await expect(row).toBeVisible({ timeout: 10_000 }); - await expect(row).toContainText("Not authenticated"); - await expect(row).toContainText( - "This adapter did not advertise a built-in login flow.", + await expect(row).not.toContainText("Not authenticated"); + await expect(row).toHaveCSS( + "height", + await page + .getByTestId("doctor-runtime-goose") + .evaluate((element) => getComputedStyle(element).height), ); - await expect(row).not.toContainText("Connect account"); + await page.getByTestId("doctor-runtime-menu-claude").click(); + await expect( + page.getByRole("menuitem", { name: "Instructions" }), + ).toBeVisible(); + await expect( + page.getByRole("menuitem", { name: "Sign in with ChatGPT" }), + ).toHaveCount(0); }); }); diff --git a/desktop/tests/e2e/profile.spec.ts b/desktop/tests/e2e/profile.spec.ts index c478faaf84..1daa936518 100644 --- a/desktop/tests/e2e/profile.spec.ts +++ b/desktop/tests/e2e/profile.spec.ts @@ -1491,11 +1491,11 @@ test("supports webview zoom keyboard shortcuts", async ({ page }) => { }); }); -test("shows doctor checks for local CLI tooling", async ({ page }) => { +test("shows agent runtimes in agent settings", async ({ page }) => { await page.goto("/"); - await openSettings(page, "doctor"); + await openSettings(page, "agents"); - await expect(page.getByTestId("settings-doctor")).toBeVisible(); + await expect(page.getByTestId("settings-agent-runtimes")).toBeVisible(); await expect(page.getByTestId("doctor-runtime-goose")).toContainText("Goose"); }); diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index b8f29be07f..84bd71f1c5 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -120,6 +120,7 @@ type MockBridgeOptions = { connectAcpRuntimeResult?: { launched: boolean }; connectAcpRuntimeDelayMs?: number; connectAcpRuntimeError?: string; + installAcpRuntimeDelayMs?: number; /** Override the result returned by the `install_acp_runtime` mock command. * Pass `{ success: false, steps: [...] }` to exercise error/Retry states. */ installAcpRuntimeResult?: { diff --git a/desktop/tests/helpers/settings.ts b/desktop/tests/helpers/settings.ts index 7763905a39..301e622719 100644 --- a/desktop/tests/helpers/settings.ts +++ b/desktop/tests/helpers/settings.ts @@ -11,8 +11,7 @@ type SettingsSection = | "tokens" | "community-members" | "mobile" - | "updates" - | "doctor"; + | "updates"; export async function openProfileMenu(page: Page) { await page.getByTestId("open-settings").click(); From fee252e3c5a8d1a6f16a45e6d4a7fde9c4c6eedc Mon Sep 17 00:00:00 2001 From: kenny lopez Date: Fri, 17 Jul 2026 18:26:33 +0200 Subject: [PATCH 2/4] Address agent runtime review feedback --- crates/buzz-acp/src/setup_mode.rs | 64 ++++++++++++-- .../settings/ui/DoctorSettingsPanel.tsx | 88 ++++++++++++++----- desktop/src/testing/e2eBridge.ts | 5 ++ desktop/tests/e2e/doctor-states.spec.ts | 61 +++++++++++++ 4 files changed, 191 insertions(+), 27 deletions(-) diff --git a/crates/buzz-acp/src/setup_mode.rs b/crates/buzz-acp/src/setup_mode.rs index 16b89fce7e..dda7857260 100644 --- a/crates/buzz-acp/src/setup_mode.rs +++ b/crates/buzz-acp/src/setup_mode.rs @@ -113,7 +113,7 @@ pub(crate) enum RequirementPayload { /// One-line stderr excerpt identifying the parse error. diagnostic: String, }, - /// Git for Windows is missing; open Doctor for the installation guide. + /// Git for Windows is missing; open Agent runtimes for the installation guide. GitBash, } @@ -139,7 +139,7 @@ impl RequirementPayload { .map(String::as_str) .unwrap_or("the agent"); format!( - "install the {} ACP adapter (open Doctor in Settings to diagnose)", + "install the {} ACP adapter (open Agent runtimes in Settings to diagnose)", harness ) } @@ -149,7 +149,7 @@ impl RequirementPayload { .map(String::as_str) .unwrap_or("the agent"); format!( - "reinstall the {} ACP adapter — the installed version is outdated (open Doctor in Settings to diagnose)", + "reinstall the {} ACP adapter — the installed version is outdated (open Agent runtimes in Settings to diagnose)", harness ) } @@ -159,7 +159,7 @@ impl RequirementPayload { .map(String::as_str) .unwrap_or("the agent"); format!( - "install {} CLI (open Doctor in Settings to diagnose)", + "install {} CLI (open Agent runtimes in Settings to diagnose)", harness ) } @@ -168,7 +168,10 @@ impl RequirementPayload { .first() .map(String::as_str) .unwrap_or("the agent"); - format!("install {} (open Doctor in Settings to diagnose)", harness) + format!( + "install {} (open Agent runtimes in Settings to diagnose)", + harness + ) } }, RequirementPayload::CliConfigInvalid { @@ -184,7 +187,7 @@ impl RequirementPayload { ) } RequirementPayload::GitBash => { - "install Git for Windows (open Doctor in Settings to diagnose)".to_string() + "install Git for Windows (open Agent runtimes in Settings to diagnose)".to_string() } } } @@ -264,7 +267,7 @@ impl SetupPayload { .any(|r| matches!(r, RequirementPayload::CliConfigInvalid { .. })); let footer = if has_doctor_requirement { - "Open Doctor in the Buzz app, install Git for Windows, then re-check and restart the agent.".to_string() + "Open Agent runtimes in Settings, install Git for Windows, then re-check and restart the agent.".to_string() } else if all_external { // All requirements are external config files — Edit Agent cannot // help. Don't send the user there. @@ -743,6 +746,53 @@ mod tests { ); } + #[test] + fn nudge_body_runtime_install_copy_points_to_agent_runtimes() { + for availability in [ + AcpAvailabilityStatus::AdapterMissing, + AcpAvailabilityStatus::AdapterOutdated, + AcpAvailabilityStatus::CliMissing, + AcpAvailabilityStatus::NotInstalled, + ] { + let payload = SetupPayload { + agent_name: "Codex".to_string(), + agent_pubkey: "test".to_string(), + requirements: vec![RequirementPayload::CliLogin { + probe_args: vec!["codex".to_string()], + setup_copy: "run `codex login`".to_string(), + availability, + }], + }; + let body = payload.nudge_body(); + assert!( + body.contains("Agent runtimes in Settings"), + "runtime install nudge must point to Agent runtimes; got: {body:?}" + ); + assert!( + !body.contains("Doctor"), + "runtime install nudge must not point to the removed Doctor section; got: {body:?}" + ); + } + } + + #[test] + fn nudge_body_git_bash_copy_points_to_agent_runtimes() { + let payload = SetupPayload { + agent_name: "Buzz Agent".to_string(), + agent_pubkey: "test".to_string(), + requirements: vec![RequirementPayload::GitBash], + }; + let body = payload.nudge_body(); + assert!( + body.contains("Open Agent runtimes in Settings"), + "Git Bash nudge must point to Agent runtimes; got: {body:?}" + ); + assert!( + !body.contains("Doctor"), + "Git Bash nudge must not point to the removed Doctor section; got: {body:?}" + ); + } + #[test] fn nudge_body_empty_requirements_falls_back_to_generic() { let payload = SetupPayload { diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx index 5cd5aee69c..84b02298cc 100644 --- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx +++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx @@ -58,32 +58,25 @@ function RuntimeLogo({ runtime }: { runtime: AcpRuntimeCatalogEntry }) { } function RuntimeOverflowMenu({ + authMethods, connectingMethodId, isConnecting, onConnect, runtime, }: { + authMethods: AcpAuthMethod[]; connectingMethodId: string | null; isConnecting: boolean; onConnect: (method: AcpAuthMethod) => void; runtime: AcpRuntimeCatalogEntry; }) { - const canConnectAccount = - runtime.availability === "available" && - runtime.authStatus.status === "logged_out"; - const authMethodsQuery = useAcpAuthMethodsQuery(runtime.id, { - enabled: canConnectAccount, - }); - const methods = canConnectAccount - ? (authMethodsQuery.data?.methods ?? []) - : []; const hasInstructions = runtime.installInstructionsUrl.trim().length > 0 && (runtime.availability !== "available" || runtime.authStatus.status === "logged_out" || runtime.authStatus.status === "config_invalid"); const hasActions = - runtime.nodeRequired || hasInstructions || methods.length > 0; + runtime.nodeRequired || hasInstructions || authMethods.length > 0; if (!hasActions) { return null; @@ -105,7 +98,7 @@ function RuntimeOverflowMenu({ align="end" onCloseAutoFocus={(event) => event.preventDefault()} > - {methods.map((method) => ( + {authMethods.map((method) => ( void; onInstall: () => void; runtime: AcpRuntimeCatalogEntry; }) { const isAvailable = runtime.availability === "available"; const canInstall = runtime.canAutoInstall && !runtime.nodeRequired; const isOn = isAvailable || installSuccess; - const connectMutation = useConnectAcpRuntimeMutation(); - const isConnecting = connectMutation.isPending; const isWorking = isInstalling || isConnecting; return (
- connectMutation.mutate({ - runtimeId: runtime.id, - methodId: method.id, - }) - } + onConnect={onConnect} runtime={runtime} /> {isWorking ? ( @@ -194,13 +189,21 @@ function RuntimeActions({ } function RuntimeHeader({ + authMethods, + connectingMethodId, installSuccess, + isConnecting, isInstalling, + onConnect, onInstall, runtime, }: { + authMethods: AcpAuthMethod[]; + connectingMethodId: string | null; installSuccess: boolean; + isConnecting: boolean; isInstalling: boolean; + onConnect: (method: AcpAuthMethod) => void; onInstall: () => void; runtime: AcpRuntimeCatalogEntry; }) { @@ -211,8 +214,12 @@ function RuntimeHeader({

{runtime.label}

@@ -233,6 +240,30 @@ function RuntimeRow({ onInstall: () => void; runtime: AcpRuntimeCatalogEntry; }) { + const canConnectAccount = + runtime.availability === "available" && + runtime.authStatus.status === "logged_out"; + const authMethodsQuery = useAcpAuthMethodsQuery(runtime.id, { + enabled: canConnectAccount, + }); + const authMethods = canConnectAccount + ? (authMethodsQuery.data?.methods ?? []) + : []; + const connectMutation = useConnectAcpRuntimeMutation(); + const connectionError = connectMutation.error + ? `Couldn't connect ${runtime.label}: ${ + connectMutation.error instanceof Error + ? connectMutation.error.message + : "Connection failed." + }` + : authMethodsQuery.error + ? `Couldn't load sign-in options: ${ + authMethodsQuery.error instanceof Error + ? authMethodsQuery.error.message + : "Request failed." + }` + : null; + return (
+ connectMutation.mutate({ + runtimeId: runtime.id, + methodId: method.id, + }) + } onInstall={onInstall} runtime={runtime} /> @@ -256,6 +296,14 @@ function RuntimeRow({ {installError}

) : null} + {connectionError ? ( +

+ {connectionError} +

+ ) : null}
); diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index c6449b7d64..bb565f12ad 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -123,6 +123,7 @@ type E2eConfig = { mock?: { acpRuntimesCatalog?: RawAcpRuntimeCatalogEntry[]; acpAuthMethods?: Record; + acpAuthMethodsErrors?: Record; connectAcpRuntimeResult?: RawConnectAcpRuntimeResult; connectAcpRuntimeDelayMs?: number; connectAcpRuntimeError?: string; @@ -6606,6 +6607,10 @@ async function handleDiscoverAcpAuthMethods( config: E2eConfig | undefined, ): Promise { const runtimeId = args.runtimeId ?? ""; + const error = config?.mock?.acpAuthMethodsErrors?.[runtimeId]; + if (error) { + throw new Error(error); + } const configured = config?.mock?.acpAuthMethods?.[runtimeId]; if (configured) { return configured; diff --git a/desktop/tests/e2e/doctor-states.spec.ts b/desktop/tests/e2e/doctor-states.spec.ts index 077dff4fbe..2211548af7 100644 --- a/desktop/tests/e2e/doctor-states.spec.ts +++ b/desktop/tests/e2e/doctor-states.spec.ts @@ -594,4 +594,65 @@ test.describe("Doctor panel state screenshots", () => { page.getByRole("menuitem", { name: "Sign in with ChatGPT" }), ).toHaveCount(0); }); + + test("08-auth-method-discovery-error", async ({ page }) => { + await installMockBridge(page, { + acpRuntimesCatalog: [ + GOOSE_AVAILABLE, + CLAUDE_AVAILABLE_LOGGED_IN, + { + ...CODEX_NOT_INSTALLED, + availability: "available", + auth_status: { status: "logged_out" }, + }, + BUZZ_AGENT_AVAILABLE, + ], + acpAuthMethodsErrors: { + codex: "Could not inspect the Codex adapter.", + }, + }); + + await page.goto("/", { waitUntil: "domcontentloaded" }); + await openSettings(page, "agents"); + + await expect(page.getByTestId("doctor-runtime-error-codex")).toContainText( + "Couldn't load sign-in options: Could not inspect the Codex adapter.", + ); + }); + + test("09-connect-account-error", async ({ page }) => { + await installMockBridge(page, { + acpRuntimesCatalog: [ + GOOSE_AVAILABLE, + CLAUDE_AVAILABLE_LOGGED_IN, + { + ...CODEX_NOT_INSTALLED, + availability: "available", + auth_status: { status: "logged_out" }, + }, + BUZZ_AGENT_AVAILABLE, + ], + acpAuthMethods: { + codex: { + methods: [ + { + id: "chat-gpt", + name: "Sign in with ChatGPT", + type: "browser", + }, + ], + }, + }, + connectAcpRuntimeError: "The browser could not be opened.", + }); + + await page.goto("/", { waitUntil: "domcontentloaded" }); + await openSettings(page, "agents"); + + await page.getByTestId("doctor-runtime-menu-codex").click(); + await page.getByRole("menuitem", { name: "Sign in with ChatGPT" }).click(); + await expect(page.getByTestId("doctor-runtime-error-codex")).toContainText( + "Couldn't connect Codex: The browser could not be opened.", + ); + }); }); From 7352009c8df0f51d1790377d3fabfe3da9fa04a3 Mon Sep 17 00:00:00 2001 From: kenny lopez Date: Fri, 17 Jul 2026 18:35:04 +0200 Subject: [PATCH 3/4] Restore terminal auth guidance --- .../settings/ui/DoctorSettingsPanel.tsx | 34 ++++++++++++++--- desktop/tests/e2e/doctor-states.spec.ts | 38 +++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx index 84b02298cc..42d8a7e159 100644 --- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx +++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx @@ -240,6 +240,9 @@ function RuntimeRow({ onInstall: () => void; runtime: AcpRuntimeCatalogEntry; }) { + const [terminalLaunchMethodId, setTerminalLaunchMethodId] = React.useState< + string | null + >(null); const canConnectAccount = runtime.availability === "available" && runtime.authStatus.status === "logged_out"; @@ -276,12 +279,22 @@ function RuntimeRow({ installSuccess={installSuccess} isConnecting={connectMutation.isPending} isInstalling={isInstalling} - onConnect={(method) => - connectMutation.mutate({ - runtimeId: runtime.id, - methodId: method.id, - }) - } + onConnect={(method) => { + setTerminalLaunchMethodId(null); + connectMutation.mutate( + { + runtimeId: runtime.id, + methodId: method.id, + }, + { + onSuccess: (result) => { + if (result.launched && method.type === "terminal") { + setTerminalLaunchMethodId(method.id); + } + }, + }, + ); + }} onInstall={onInstall} runtime={runtime} /> @@ -304,6 +317,15 @@ function RuntimeRow({ {connectionError}

) : null} + {canConnectAccount && terminalLaunchMethodId ? ( +

+ Finish signing in from the Terminal window, then click Check again + to re-check {runtime.label}. +

+ ) : null}
); diff --git a/desktop/tests/e2e/doctor-states.spec.ts b/desktop/tests/e2e/doctor-states.spec.ts index 2211548af7..b672c4c71e 100644 --- a/desktop/tests/e2e/doctor-states.spec.ts +++ b/desktop/tests/e2e/doctor-states.spec.ts @@ -655,4 +655,42 @@ test.describe("Doctor panel state screenshots", () => { "Couldn't connect Codex: The browser could not be opened.", ); }); + + test("10-terminal-auth-completion-guidance", async ({ page }) => { + await installMockBridge(page, { + acpRuntimesCatalog: [ + GOOSE_AVAILABLE, + CLAUDE_AVAILABLE_LOGGED_IN, + { + ...CODEX_NOT_INSTALLED, + availability: "available", + auth_status: { status: "logged_out" }, + }, + BUZZ_AGENT_AVAILABLE, + ], + acpAuthMethods: { + codex: { + methods: [ + { + id: "terminal-login", + name: "Sign in from Terminal", + type: "terminal", + }, + ], + }, + }, + connectAcpRuntimeResult: { launched: true }, + }); + + await page.goto("/", { waitUntil: "domcontentloaded" }); + await openSettings(page, "agents"); + + await page.getByTestId("doctor-runtime-menu-codex").click(); + await page.getByRole("menuitem", { name: "Sign in from Terminal" }).click(); + await expect( + page.getByTestId("doctor-runtime-terminal-guidance-codex"), + ).toContainText( + "Finish signing in from the Terminal window, then click Check again to re-check Codex.", + ); + }); }); From c1ebdbf75e191edf35759663840d4dee6f76102c Mon Sep 17 00:00:00 2001 From: kenny lopez Date: Fri, 17 Jul 2026 20:11:20 +0200 Subject: [PATCH 4/4] Address agent runtime review feedback --- .../settings/ui/DoctorSettingsPanel.tsx | 108 ++++++++++++++++-- .../src/shared/ui/config-nudge-attachment.tsx | 32 +++--- desktop/tests/e2e/doctor-states.spec.ts | 72 +++++++++--- 3 files changed, 173 insertions(+), 39 deletions(-) diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx index 42d8a7e159..cee156ece7 100644 --- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx +++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx @@ -14,6 +14,16 @@ import type { AcpAuthMethod, AcpRuntimeCatalogEntry } from "@/shared/api/types"; import { getInstallErrorMessage } from "@/shared/lib/installError"; import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/shared/ui/alert-dialog"; import { DropdownMenu, DropdownMenuContent, @@ -111,14 +121,14 @@ function RuntimeOverflowMenu({ ))} {runtime.nodeRequired ? ( - void openUrl("https://nodejs.org")}> + void openUrl("https://nodejs.org")}> Install Node.js ) : null} {hasInstructions ? ( void openUrl(runtime.installInstructionsUrl)} + onSelect={() => void openUrl(runtime.installInstructionsUrl)} > Instructions @@ -165,7 +175,7 @@ function RuntimeActions({ {isWorking ? (
@@ -188,6 +198,44 @@ function RuntimeActions({ ); } +function RuntimeStatusChip({ runtime }: { runtime: AcpRuntimeCatalogEntry }) { + const label = + runtime.authStatus.status === "config_invalid" + ? "Config error" + : runtime.availability === "adapter_missing" + ? "Adapter needed" + : runtime.availability === "adapter_outdated" + ? "Update needed" + : runtime.availability === "cli_missing" + ? "CLI needed" + : null; + + if (!label) { + return null; + } + + const isConfigError = runtime.authStatus.status === "config_invalid"; + + return ( + <> + + + {label} + + + ); +} + function RuntimeHeader({ authMethods, connectingMethodId, @@ -211,7 +259,10 @@ function RuntimeHeader({
-

{runtime.label}

+
+

{runtime.label}

+ +
(null); + const [isUpdateWarningOpen, setIsUpdateWarningOpen] = React.useState(false); const canConnectAccount = runtime.availability === "available" && runtime.authStatus.status === "logged_out"; @@ -295,10 +347,25 @@ function RuntimeRow({ }, ); }} - onInstall={onInstall} + onInstall={() => { + if (runtime.availability === "adapter_outdated") { + setIsUpdateWarningOpen(true); + return; + } + onInstall(); + }} runtime={runtime} /> + {runtime.authStatus.status === "config_invalid" ? ( +

+ Config error: {runtime.authStatus.diagnostic} +

+ ) : null} + {installSuccess && runtime.availability !== "available" ? (

{runtime.label} installed. Checking for sign-in options... @@ -327,6 +394,30 @@ function RuntimeRow({

) : null}
+ + + + Update {runtime.label} adapter? + + This replaces the machine-wide codex-acp adapter. Older Buzz + releases using the legacy adapter may lose community access until + @zed-industries/codex-acp@0.16.0 is restored. + + + + Cancel + + Update + + + +
); } @@ -377,9 +468,10 @@ function GitBashCard({ ) : null}
{!prerequisite.available ? ( -

- Required for buzz-agent shell tools on Windows. -

+
+

Required for buzz-agent shell tools on Windows.

+

{prerequisite.installHint}

+
) : null}
diff --git a/desktop/src/shared/ui/config-nudge-attachment.tsx b/desktop/src/shared/ui/config-nudge-attachment.tsx index 321feab912..e1294b7dde 100644 --- a/desktop/src/shared/ui/config-nudge-attachment.tsx +++ b/desktop/src/shared/ui/config-nudge-attachment.tsx @@ -43,7 +43,7 @@ function requirementKey( /** * Returns true when every requirement in the nudge is a `cli_login` surface. * Non-authOnly all-cli_login cards (at least one install-state row) route to - * Doctor — install/login problems can't be fixed in Edit Agent. AuthOnly cards + * Agent runtimes — install/login problems can't be fixed in Edit Agent. AuthOnly cards * (every row is `availability === "available"`) are purely informational and * do not route anywhere. */ @@ -65,8 +65,8 @@ export function shouldOpenDoctor( /** * Returns true when the card is all-cli_login AND every requirement is in the - * `available` state (tooling installed, just needs login). In this case Doctor - * has no auth functionality and is a misleading dead-end — the card becomes + * `available` state (tooling installed, just needs login). In this case Agent + * runtimes has no auth functionality and is a misleading dead-end — the card becomes * purely informational (no trigger, no CTA, no pointer/hover affordance). */ function isAuthOnly(reqs: ConfigNudgePayload["requirements"]): boolean { @@ -140,7 +140,7 @@ function firstFocusTarget( * Mirrors `firstFocusTarget` but operates on one row — used so per-row * Edit Agent CTAs focus the field that row describes, not the first editable * field on the card. - * Returns `undefined` for `cli_login` requirements (Doctor, not Edit Agent). + * Returns `undefined` for `cli_login` requirements (Agent runtimes, not Edit Agent). */ export function focusTargetForRequirement( req: ConfigNudgePayload["requirements"][number], @@ -164,15 +164,15 @@ export function focusTargetForRequirement( * * Routing: * (A) Any card with a `git_bash` requirement, or one whose requirements are all - * install-state `cli_login`, opens Settings → Doctor. A card-level + * install-state `cli_login`, opens Settings → Agent runtimes. A card-level * Agent runtimes label in `AttachmentActions` confirms the action at rest. * (A-auth) A card whose requirements are all available `cli_login` surfaces is - * purely informational: Doctor cannot authenticate a CLI, and `setup_copy` + * purely informational: Agent runtimes cannot authenticate a CLI, and `setup_copy` * already gives the needed command. * (B) Other mixed cards open Edit Agent as the card-level fallback. Their rows * carry inline CTAs for the matching destination: install-state `cli_login` - * opens Doctor; `env_key` and `normalized_field` open Edit Agent. A - * `git_bash` row is covered by the card-level Doctor route, so it does not + * opens Agent runtimes; `env_key` and `normalized_field` open Edit Agent. A + * `git_bash` row is covered by the card-level Agent runtimes route, so it does not * render a redundant row action. */ export function ConfigNudgeCard({ @@ -196,7 +196,7 @@ export function ConfigNudgeCard({ const openDoctor = () => { if (!onOpenSettings) { console.warn( - "[ConfigNudgeCard] onOpenSettings is null — Doctor deep-link unavailable on this surface", + "[ConfigNudgeCard] onOpenSettings is null — Agent runtimes deep-link unavailable on this surface", ); } onOpenSettings?.("agents"); @@ -209,7 +209,7 @@ export function ConfigNudgeCard({ const handleOpen = () => { if (shouldOpenDoctor(nudge.requirements)) { - // Git Bash and install-state CLI requirements both resolve in Doctor. + // Git Bash and install-state CLI requirements both resolve in Agent runtimes. // Informational-only cards never mount this trigger. openDoctor(); } else { @@ -219,7 +219,7 @@ export function ConfigNudgeCard({ }; const handleOpenDoctor = (e: React.MouseEvent) => { - // (B) Per-row Doctor CTA — stop propagation so the card trigger doesn't + // (B) Per-row Agent runtimes CTA — stop propagation so the card trigger doesn't // double-fire to Edit Agent on mixed cards. e.stopPropagation(); openDoctor(); @@ -266,7 +266,7 @@ export function ConfigNudgeCard({ ))} - {/* (A) Doctor-routed cards have one card-level CTA. Informational-only + {/* (A) Agent-runtime-routed cards have one card-level CTA. Informational-only cards have none; other mixed cards render their own row CTAs. */} {opensDoctor && !informationalOnly && ( @@ -354,11 +354,11 @@ function RequirementRow({ {cliLoginMessage(requirement)} - {/* (B) Per-row Doctor CTA — shown only on mixed cards where the + {/* (B) Per-row Agent runtimes CTA — shown only on mixed cards where the card-level trigger opens Edit Agent (not auth-only cards). When - allCliLogin is true the card trigger already routes to Doctor; the + allCliLogin is true the card trigger already routes to Agent runtimes; the per-row button is redundant and is suppressed. Also suppressed for - `available` cli_login rows — Doctor has no auth functionality and + `available` cli_login rows — Agent runtimes has no auth functionality and the setup_copy already provides the exact login command. stopPropagation prevents double-fire on mixed cards where both card and row CTAs are visible. */} @@ -383,7 +383,7 @@ function RequirementRow({ ); case "cli_config_invalid": { // Config-invalid rows are purely informational — the user must edit an - // external file. No Doctor CTA (Doctor can't repair ~/.codex/config.toml) + // external file. No Agent runtimes CTA (Buzz can't repair ~/.codex/config.toml) // and no Edit Agent CTA (the field isn't managed by Buzz). const cli = requirement.probe_args[0] ?? "the CLI"; const configFile = `~/.${cli}/config.toml`; diff --git a/desktop/tests/e2e/doctor-states.spec.ts b/desktop/tests/e2e/doctor-states.spec.ts index b672c4c71e..1a724f719a 100644 --- a/desktop/tests/e2e/doctor-states.spec.ts +++ b/desktop/tests/e2e/doctor-states.spec.ts @@ -15,7 +15,7 @@ const SHOTS = "test-results/screenshots-doctor"; const GOOSE_AVAILABLE = { id: "goose", label: "Goose", - avatar_url: "https://goose-docs.ai/img/logo_dark.png", + avatar_url: "", availability: "available", command: "goose", binary_path: "/usr/local/bin/goose", @@ -54,8 +54,7 @@ const BUZZ_AGENT_AVAILABLE = { const CLAUDE_AVAILABLE_LOGGED_IN = { id: "claude", label: "Claude Code", - avatar_url: - "https://anthropic.gallerycdn.vsassets.io/extensions/anthropic/claude-code/2.1.77/1773707456892/Microsoft.VisualStudio.Services.Icons.Default", + avatar_url: "", availability: "available", command: "claude-agent-acp", binary_path: "/usr/local/bin/claude-agent-acp", @@ -77,8 +76,7 @@ const CLAUDE_AVAILABLE_LOGGED_IN = { const CODEX_NOT_INSTALLED = { id: "codex", label: "Codex", - avatar_url: - "https://openai.gallerycdn.vsassets.io/extensions/openai/chatgpt/26.5313.41514/1773706730621/Microsoft.VisualStudio.Services.Icons.Default", + avatar_url: "", availability: "not_installed", command: null, binary_path: null, @@ -299,8 +297,8 @@ test.describe("Doctor panel state screenshots", () => { }); /** - * 03 — a runtime with invalid configuration remains compact and exposes its - * setup instructions from overflow instead of expanding the row. + * 03 — a runtime with invalid configuration exposes its diagnostic and keeps + * setup instructions in overflow. */ test("03-auth-config-error", async ({ page }) => { const diagnostic = @@ -323,13 +321,13 @@ test.describe("Doctor panel state screenshots", () => { const row = page.getByTestId("doctor-runtime-claude"); await expect(row).toBeVisible({ timeout: 10_000 }); - await expect(row).not.toContainText("Config error"); - await expect(row).not.toContainText("error loading configuration"); - await expect(row).toHaveCSS( - "height", - await page - .getByTestId("doctor-runtime-goose") - .evaluate((element) => getComputedStyle(element).height), + await expect(page.getByTestId("doctor-runtime-status-claude")).toHaveText( + "Config error", + ); + await expect( + page.getByTestId("doctor-runtime-config-error-claude"), + ).toContainText( + "Config error: error loading configuration: ~/.claude/settings.json: unknown key foo", ); await page.getByTestId("doctor-runtime-menu-claude").click(); await expect( @@ -371,7 +369,9 @@ test.describe("Doctor panel state screenshots", () => { const toggle = page.getByTestId("doctor-runtime-toggle-codex"); await expect(toggle).not.toBeChecked(); await expect(toggle).toBeDisabled(); - await expect(row).not.toContainText("Adapter needed"); + await expect(page.getByTestId("doctor-runtime-status-codex")).toHaveText( + "Adapter needed", + ); await expect(row).not.toContainText("Node.js is required"); await expect(row).toHaveCSS( "height", @@ -456,6 +456,7 @@ test.describe("Doctor panel state screenshots", () => { await toggle.click(); const loading = page.getByTestId("doctor-runtime-loading-codex"); await expect(loading).toBeVisible(); + await expect(loading).toContainText("Codex installing"); await expect(toggle).toHaveCount(0); // After failure: the toggle returns to off and the error is visible. @@ -546,6 +547,7 @@ test.describe("Doctor panel state screenshots", () => { await page.getByRole("menuitem", { name: "Sign in with ChatGPT" }).click(); const loading = page.getByTestId("doctor-runtime-loading-codex"); await expect(loading).toBeVisible(); + await expect(loading).toContainText("Codex connecting"); await expect(page.getByTestId("doctor-runtime-toggle-codex")).toHaveCount( 0, ); @@ -693,4 +695,44 @@ test.describe("Doctor panel state screenshots", () => { "Finish signing in from the Terminal window, then click Check again to re-check Codex.", ); }); + + test("11-outdated-adapter-warning", async ({ page }) => { + await installMockBridge(page, { + acpRuntimesCatalog: [ + GOOSE_AVAILABLE, + CLAUDE_AVAILABLE_LOGGED_IN, + { + ...CODEX_NOT_INSTALLED, + availability: "adapter_outdated", + binary_path: "/usr/local/bin/codex-acp", + underlying_cli_path: "/usr/local/bin/codex", + can_auto_install: true, + }, + BUZZ_AGENT_AVAILABLE, + ], + installAcpRuntimeDelayMs: 250, + }); + + await page.goto("/", { waitUntil: "domcontentloaded" }); + await openSettings(page, "agents"); + + await expect(page.getByTestId("doctor-runtime-status-codex")).toHaveText( + "Update needed", + ); + await page.getByTestId("doctor-runtime-toggle-codex").click(); + + const dialog = page.getByRole("alertdialog"); + await expect(dialog).toContainText("Update Codex adapter?"); + await expect(dialog).toContainText( + "Older Buzz releases using the legacy adapter may lose community access", + ); + await expect(page.getByTestId("doctor-runtime-loading-codex")).toHaveCount( + 0, + ); + + await page.getByTestId("doctor-runtime-confirm-update-codex").click(); + const loading = page.getByTestId("doctor-runtime-loading-codex"); + await expect(loading).toBeVisible(); + await expect(loading).toContainText("Codex installing"); + }); });