Improve agent runtime settings (#2026)

This commit is contained in:
klopez4212
2026-07-19 19:01:38 +00:00
committed by GitHub
parent 2e936d439c
commit 2b0f5e9f80
20 changed files with 930 additions and 631 deletions
+57 -7
View File
@@ -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 {
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg width="2048" height="2048" viewBox="0 0 2048 2048" xmlns="http://www.w3.org/2000/svg">
<rect x="200" y="200" width="1648" height="1648" rx="400" ry="400" fill="#fff"/>
<g transform="translate(128 94) scale(3)">
<path fill="#000" d="M455.145 485.687c18.641-4.048 38.414-15.065 38.414-15.065l-34.607-28.507c-17.108-14.081-31.67-30.999-43.054-50.007-15.731-26.263-37.467-48.425-63.413-64.663l-12.672-7.379c-4.342-3.018-7.369-7.722-7.8-13.029-.276-3.422.544-6.481 2.457-9.173 6.598-9.3 40.702-49.767 46.952-54.938 8.049-6.653 17.018-12.187 25.339-18.523 1.183-.902 2.37-1.799 3.539-2.714.041-.041.1-.073.136-.109 2.67-2.108 5.19-4.347 7.193-7.002 7.233-8.376 8.991-15.781 9.422-19.058-.979-3.154-3.898-10.206-12.033-18.341 5.099.313 11.272 4.346 16.869 9.091 3.757-6.005 7.691-12.35 11.62-18.708 2.624-4.247-1.269-7.424-1.378-7.523l-.022-.005v-.018c-.1-.109-3.277-4.002-7.514-1.378-9.06 5.606-18.115 11.217-26.168 16.315 0 0-9.545-.199-20.848 9.563-2.66 2.008-4.899 4.527-7.002 7.192-.041.041-.077.095-.109.136-.92 1.165-1.817 2.352-2.714 3.54-6.341 8.325-11.87 17.29-18.523 25.339-5.167 6.254-45.638 40.353-54.938 46.952-2.692 1.913-5.747 2.737-9.173 2.456-5.302-.426-10.011-3.458-13.03-7.799l-7.378-12.672c-16.238-25.955-38.4-47.682-64.663-63.413-19.008-11.384-35.922-25.951-50.007-43.055l-28.516-34.607s-11.022 19.769-15.065 38.414c5.624 6.839 20.295 23.812 37.372 36.774-18.36-8.634-31.933-15.31-42.638-20.893-1.65 12.209-1.006 30.673.693 44.972 11.589 5.071 31.321 12.884 51.408 17.036-16.067 3.757-33.692 4.441-47.302 4.174 2.398 8.851 5.733 17.879 10.22 26.961 1.908 4.201 3.997 8.303 6.254 12.318 7.202 1.981 35.478 6.1 50.524 2.719-14.969 5.344-40.1 14.322-40.1 14.322 19.302 24.015 40.635 43.553 40.635 43.553 32.449-17.448 39.846-19.832 64.183-35.749-39.411 32.069-49.64 45.162-60.703 58.618l-7.709 10.818c-4.006 5.62-7.482 11.589-10.387 17.847-9.721 20.916-23.481 65.484-23.481 65.484-2.456 7.768 3.236 13.461 10.777 10.778 0 0 44.56-13.76 65.484-23.481 6.255-2.91 12.233-6.386 17.848-10.388l10.818-7.709c3.63-2.986 7.238-5.914 11.199-9.367 0 0 27.233 32.263 55.227 53.487 0 0 8.979-25.13 14.322-40.1-3.386 15.051.734 43.327 2.719 50.524 4.006 2.257 8.117 4.346 12.318 6.254 9.087 4.491 18.11 7.822 26.962 10.22-.263-13.614.417-31.235 4.174-47.301 4.151 20.086 11.96 39.823 17.036 51.407 14.298 1.7 32.762 2.343 44.972.693-5.584-10.704-12.26-24.287-20.893-42.637 12.961 17.077 29.934 31.747 36.773 37.371l-.014-.013z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+1 -1
View File
@@ -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;
};
+4
View File
@@ -12,6 +12,10 @@ type SettingsRouteSearch = {
function validateSettingsSearch(
search: Record<string, unknown>,
): SettingsRouteSearch {
if (search.section === "doctor") {
return { section: "agents" };
}
return {
section: isSettingsSection(search.section) ? search.section : undefined,
};
@@ -624,7 +624,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 &gt; Doctor to set it up.
Visit Settings &gt; Agents to set it up.
</p>
) : null;
const advancedFieldsTransition = shouldReduceMotion
@@ -815,8 +815,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 &gt; Doctor.
No compatible agent runtimes detected yet. You can finish setup now
and come back later in Settings &gt; Agents.
</p>
)}
@@ -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 ? (
<AvatarImage
alt={`${label} avatar`}
className="object-cover"
className={cn("object-cover", imageClassName)}
data-testid={testId ? `${testId}-image` : undefined}
onLoadingStatusChange={(status) => {
if (status === "error") setFailedSrc(liveSrc);
File diff suppressed because it is too large Load Diff
@@ -18,7 +18,6 @@ import {
ShieldAlert,
Smartphone,
Smile,
Stethoscope,
Sun,
SunMoon,
UserRound,
@@ -100,8 +99,7 @@ export type SettingsSection =
| "custom-emoji"
| "local-archive"
| "mobile"
| "updates"
| "doctor";
| "updates";
export const DEFAULT_SETTINGS_SECTION: SettingsSection = "profile";
@@ -121,7 +119,6 @@ const SETTINGS_SECTION_VALUES: readonly SettingsSection[] = [
"local-archive",
"mobile",
"updates",
"doctor",
];
export function isSettingsSection(value: unknown): value is SettingsSection {
@@ -233,12 +230,6 @@ export const settingsSections: SettingsSectionDescriptor[] = [
label: "Updates",
icon: Download,
},
{
value: "doctor",
label: "Doctor",
icon: Stethoscope,
featureGate: "doctor",
},
];
function formatThemeLabel(name: string): string {
@@ -822,6 +813,7 @@ export function renderSettingsSection(
return (
<div className="space-y-12">
<PreventSleepSettingsCard />
<DoctorSettingsPanel />
<GlobalAgentConfigSettingsCard />
</div>
);
@@ -849,8 +841,6 @@ export function renderSettingsSection(
return <MobilePairingCard currentPubkey={props.currentPubkey} />;
case "updates":
return <UpdateChecker />;
case "doctor":
return <DoctorSettingsPanel />;
default: {
const exhaustiveCheck: never = section;
return exhaustiveCheck;
@@ -66,14 +66,7 @@ const settingsNavGroups: Array<{
},
{
label: "App",
sections: [
"agents",
"compute",
"experimental",
"mobile",
"updates",
"doctor",
],
sections: ["agents", "compute", "experimental", "mobile", "updates"],
},
];
@@ -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
* "Open Doctor →" label in `AttachmentActions` confirms the action at rest.
* 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,10 +196,10 @@ 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?.("doctor");
onOpenSettings?.("agents");
};
const openEditAgent = (focus?: EditAgentFocusTarget) => {
@@ -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,11 +266,13 @@ export function ConfigNudgeCard({
))}
</div>
</AttachmentContent>
{/* (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 && (
<AttachmentActions className="items-end self-end">
<span className="text-xs text-muted-foreground">Open Doctor </span>
<span className="text-xs text-muted-foreground">
Open Agent runtimes
</span>
</AttachmentActions>
)}
{/* Informational-only cards are purely informational — no trigger, no routing. */}
@@ -278,7 +280,7 @@ export function ConfigNudgeCard({
<AttachmentTrigger
aria-label={
opensDoctor
? `Open Doctor settings for ${nudge.agent_name}`
? `Open Agent runtimes settings for ${nudge.agent_name}`
: `Open Edit Agent for ${nudge.agent_name}`
}
onClick={handleOpen}
@@ -352,11 +354,11 @@ function RequirementRow({
<span className="flex-1 [overflow-wrap:anywhere]">
{cliLoginMessage(requirement)}
</span>
{/* (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. */}
@@ -366,7 +368,7 @@ function RequirementRow({
onClick={onOpenDoctor}
type="button"
>
Open Doctor
Open Agent runtimes
</button>
)}
</div>
@@ -381,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`;
+2 -2
View File
@@ -9,7 +9,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-none transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
@@ -17,7 +17,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-none ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
+13 -3
View File
@@ -128,11 +128,13 @@ type E2eConfig = {
acpRuntimesCatalog?: RawAcpRuntimeCatalogEntry[];
acpRuntimesDelayMs?: number;
acpAuthMethods?: Record<string, RawAcpAuthMethodsResult>;
acpAuthMethodsErrors?: Record<string, string>;
acpAuthMethodsError?: string;
connectAcpRuntimeResult?: RawConnectAcpRuntimeResult;
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.
@@ -6700,11 +6702,15 @@ async function handleDiscoverAcpAuthMethods(
args: { runtimeId?: string },
config: E2eConfig | undefined,
): Promise<RawAcpAuthMethodsResult> {
const error = config?.mock?.acpAuthMethodsError;
if (error) {
throw new Error(error);
const globalError = config?.mock?.acpAuthMethodsError;
if (globalError) {
throw new Error(globalError);
}
const runtimeId = args.runtimeId ?? "";
const perRuntimeError = config?.mock?.acpAuthMethodsErrors?.[runtimeId];
if (perRuntimeError) {
throw new Error(perRuntimeError);
}
const configured = config?.mock?.acpAuthMethods?.[runtimeId];
if (configured) {
return configured;
@@ -6750,6 +6756,10 @@ async function handleInstallAcpRuntime(
},
config: E2eConfig | undefined,
): Promise<RawInstallRuntimeResult> {
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);
@@ -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();
+371 -55
View File
@@ -111,9 +111,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 +232,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 +251,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 +273,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 +297,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 exposes its diagnostic and keeps
* setup instructions in overflow.
*/
test("03-auth-config-error", async ({ page }) => {
const diagnostic =
@@ -192,12 +317,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(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(
page.getByRole("menuitem", { name: "Instructions" }),
).toBeVisible();
await page.keyboard.press("Escape");
await row.scrollIntoViewIfNeeded();
await waitForAnimations(page);
@@ -205,9 +341,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 +362,28 @@ 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(page.getByTestId("doctor-runtime-status-codex")).toHaveText(
"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,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).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();
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(loading).toContainText("Codex installing");
await expect(toggle).toHaveCount(0);
// 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 });
// 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 +470,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 +492,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 +527,37 @@ 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(loading).toContainText("Codex connecting");
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 +577,162 @@ 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);
});
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.",
);
});
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.",
);
});
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");
});
});
+3 -3
View File
@@ -1537,11 +1537,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");
});
+1
View File
@@ -134,6 +134,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?: {
+1 -2
View File
@@ -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();