From 6e8154516822ea9e16d24df15a79164360b9e31e Mon Sep 17 00:00:00 2001 From: kenny lopez Date: Fri, 24 Jul 2026 09:54:59 -0700 Subject: [PATCH] Filter hidden harnesses from remaining pickers Signed-off-by: kenny lopez --- .../lib/runtimeVisibilityPreference.test.mjs | 22 +++++++++++ .../agents/lib/runtimeVisibilityPreference.ts | 32 ++++++++++++++++ .../onboarding/ui/DefaultConfigStep.tsx | 11 ++++-- .../ui/onboardingRuntimeSelection.test.mjs | 12 ++++++ .../ui/onboardingRuntimeSelection.ts | 4 +- .../ui/ChannelTemplatesSettingsCard.tsx | 28 +++++++++++++- .../e2e/onboarding-agent-defaults.spec.ts | 37 +++++++++++++++++++ 7 files changed, 140 insertions(+), 6 deletions(-) diff --git a/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs b/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs index 7230d4942..9bf801493 100644 --- a/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs +++ b/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs @@ -8,6 +8,7 @@ import { nextDisabledAcpRuntimeIds, parseDisabledAcpRuntimeIds, readDisabledAcpRuntimeIds, + runtimesForAcpConfigurationPicker, runtimesForImplicitAcpSelection, visibleAcpRuntimeSeedForCreate, } from "./runtimeVisibilityPreference.ts"; @@ -56,6 +57,27 @@ test("disabled runtimes are removed from selectable catalog entries", () => { ); }); +test("configuration pickers hide new choices but preserve the current runtime", () => { + const runtimes = [ + { id: "buzz-agent", label: "Buzz Agent" }, + { id: "goose", label: "Goose" }, + { id: "codex", label: "Codex" }, + ]; + + assert.deepEqual( + runtimesForAcpConfigurationPicker( + runtimes, + ["buzz-agent", "goose"], + "goose", + ), + [runtimes[2], runtimes[1]], + ); + assert.deepEqual( + runtimesForAcpConfigurationPicker(runtimes, ["buzz-agent", "goose"], null), + [runtimes[2]], + ); +}); + test("create seeds replace hidden runtimes but preserve selectable ones", () => { const runtimes = [{ id: "buzz-agent" }, { id: "goose" }]; assert.equal( diff --git a/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts b/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts index 89932e603..040831c28 100644 --- a/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts +++ b/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts @@ -95,6 +95,38 @@ export function filterEnabledAcpRuntimes( ); } +/** + * Filter new configuration choices while preserving one explicitly saved + * runtime as a current-only option. + */ +export function runtimesForAcpConfigurationPicker( + runtimes: readonly T[], + disabledRuntimeIds: readonly string[], + currentRuntimeId?: string | null, +): T[] { + const selectableRuntimes = filterEnabledAcpRuntimes( + runtimes, + disabledRuntimeIds, + ); + const normalizedCurrentRuntimeId = normalizeRuntimeId(currentRuntimeId ?? ""); + if ( + !normalizedCurrentRuntimeId || + selectableRuntimes.some( + (runtime) => + normalizeRuntimeId(runtime.id) === normalizedCurrentRuntimeId, + ) + ) { + return selectableRuntimes; + } + + const currentRuntime = runtimes.find( + (runtime) => normalizeRuntimeId(runtime.id) === normalizedCurrentRuntimeId, + ); + return currentRuntime + ? [...selectableRuntimes, currentRuntime] + : selectableRuntimes; +} + /** * Apply visibility only when the app is choosing a runtime implicitly. * Existing definitions pinned to a runtime remain runnable. diff --git a/desktop/src/features/onboarding/ui/DefaultConfigStep.tsx b/desktop/src/features/onboarding/ui/DefaultConfigStep.tsx index 50887f08a..41f7f1d43 100644 --- a/desktop/src/features/onboarding/ui/DefaultConfigStep.tsx +++ b/desktop/src/features/onboarding/ui/DefaultConfigStep.tsx @@ -10,6 +10,7 @@ import { } from "@/features/agents/ui/AgentConfigFields"; import { resetConfigForHarnessChange } from "@/features/agents/ui/agentConfigOptions"; import { AgentDropdownSelect } from "@/features/agents/ui/agentConfigControls"; +import { useDisabledAcpRuntimeIds } from "@/features/agents/lib/runtimeVisibilityPreference"; import { createSaveCoalescer } from "./saveCoalescer"; import { getBakedBuildEnv, type BakedEnvEntry } from "@/shared/api/tauri"; import { @@ -127,14 +128,16 @@ function AgentDefaultsSection({ () => new Set(effectiveReadyRuntimeIds), [effectiveReadyRuntimeIds], ); + const disabledRuntimeIds = useDisabledAcpRuntimeIds(); // Setup already confirmed readiness. Re-filter only for onboarding // visibility here; a transient auth recheck must not invalidate that handoff. const readyRuntimes = React.useMemo( () => - getVisibleOnboardingRuntimes(runtimesQuery.data ?? []).filter((runtime) => - readyRuntimeIdSet.has(runtime.id), - ), - [readyRuntimeIdSet, runtimesQuery.data], + getVisibleOnboardingRuntimes( + runtimesQuery.data ?? [], + disabledRuntimeIds, + ).filter((runtime) => readyRuntimeIdSet.has(runtime.id)), + [disabledRuntimeIds, readyRuntimeIdSet, runtimesQuery.data], ); const selectedRuntime = React.useMemo( () => diff --git a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.test.mjs b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.test.mjs index 221702ebb..c49c10db8 100644 --- a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.test.mjs +++ b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.test.mjs @@ -34,6 +34,18 @@ test("visible onboarding runtimes use the product order", () => { ); }); +test("onboarding defaults exclude device-disabled harnesses", () => { + const runtimes = [ + runtime("codex", "available", "logged_in"), + runtime("claude", "available", "logged_in"), + ]; + + assert.deepEqual( + getVisibleOnboardingRuntimes(runtimes, ["claude"]).map(({ id }) => id), + ["codex"], + ); +}); + test("readiness requires an available and authenticated runtime", () => { assert.equal( runtimeIsReadyForOnboarding(runtime("claude", "available", "logged_in")), diff --git a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts index cd491dfcc..591ea25ba 100644 --- a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts +++ b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts @@ -1,4 +1,5 @@ import type { AcpRuntimeCatalogEntry } from "@/shared/api/types"; +import { runtimesForAcpConfigurationPicker } from "@/features/agents/lib/runtimeVisibilityPreference"; export const ONBOARDING_RUNTIME_ORDER = [ "claude", @@ -25,8 +26,9 @@ export function runtimeIsReadyForOnboarding(runtime: AcpRuntimeCatalogEntry) { export function getVisibleOnboardingRuntimes( runtimes: readonly AcpRuntimeCatalogEntry[], + disabledRuntimeIds: readonly string[] = [], ) { - return runtimes + return runtimesForAcpConfigurationPicker(runtimes, disabledRuntimeIds) .filter((runtime) => runtimeIsVisibleInOnboarding(runtime.id)) .sort( (left, right) => diff --git a/desktop/src/features/settings/ui/ChannelTemplatesSettingsCard.tsx b/desktop/src/features/settings/ui/ChannelTemplatesSettingsCard.tsx index 82680ae5a..6e619a26e 100644 --- a/desktop/src/features/settings/ui/ChannelTemplatesSettingsCard.tsx +++ b/desktop/src/features/settings/ui/ChannelTemplatesSettingsCard.tsx @@ -16,6 +16,10 @@ import { usePersonasQuery, useTeamsQuery, } from "@/features/agents/hooks"; +import { + runtimesForAcpConfigurationPicker, + useDisabledAcpRuntimeIds, +} from "@/features/agents/lib/runtimeVisibilityPreference"; import { useChannelTemplatesQuery, useCreateChannelTemplateMutation, @@ -292,6 +296,7 @@ function TemplateFormDialog({ const teamsQuery = useTeamsQuery(); const providersQuery = useAvailableAcpRuntimes(); const runtimes = providersQuery.data ?? []; + const disabledRuntimeIds = useDisabledAcpRuntimeIds(); const [name, setName] = React.useState(""); const [description, setDescription] = React.useState(""); @@ -559,6 +564,7 @@ function TemplateFormDialog({ personaRuntimes={personaRuntimes} providers={runtimes} providersLoading={providersQuery.isLoading} + disabledRuntimeIds={disabledRuntimeIds} selectedPersonaIds={selectedPersonaIds} selectedTeamIds={selectedTeamIds} teamRuntimes={teamRuntimes} @@ -638,6 +644,7 @@ function TemplateTeamSelector({ } function RuntimeAssignments({ + disabledRuntimeIds, isPending, onPersonaRuntimeChange, onTeamRuntimeChange, @@ -650,6 +657,7 @@ function RuntimeAssignments({ teamRuntimes, teams, }: { + disabledRuntimeIds: readonly string[]; isPending: boolean; onPersonaRuntimeChange: (personaId: string, runtimeId: string) => void; onTeamRuntimeChange: (teamId: string, runtimeId: string) => void; @@ -697,6 +705,7 @@ function RuntimeAssignments({ onChange={(runtimeId) => onPersonaRuntimeChange(persona.id, runtimeId) } + disabledRuntimeIds={disabledRuntimeIds} providers={providers} value={personaRuntimes[persona.id] ?? ""} /> @@ -708,6 +717,7 @@ function RuntimeAssignments({ icon="team" label={team.name} onChange={(runtimeId) => onTeamRuntimeChange(team.id, runtimeId)} + disabledRuntimeIds={disabledRuntimeIds} providers={providers} value={teamRuntimes[team.id] ?? ""} /> @@ -721,6 +731,7 @@ function RuntimeAssignments({ function RuntimeRow({ avatarUrl, disabled, + disabledRuntimeIds, icon, label, onChange, @@ -729,12 +740,24 @@ function RuntimeRow({ }: { avatarUrl?: string | null | undefined; disabled: boolean; + disabledRuntimeIds: readonly string[]; icon?: "team"; label: string; onChange: (runtimeId: string) => void; providers: AcpRuntime[]; value: string; }) { + const runtimeOptions = runtimesForAcpConfigurationPicker( + providers, + disabledRuntimeIds, + value, + ); + const selectableRuntimeIds = new Set( + runtimesForAcpConfigurationPicker(providers, disabledRuntimeIds).map( + (runtime) => runtime.id, + ), + ); + return (
@@ -756,9 +779,12 @@ function RuntimeRow({ value={value} > - {providers.map((runtime) => ( + {runtimeOptions.map((runtime) => ( ))} diff --git a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts index 1c3e86f13..c7909ef42 100644 --- a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts +++ b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from "@playwright/test"; +import { ACP_RUNTIME_VISIBILITY_STORAGE_KEY } from "@/features/agents/lib/runtimeVisibilityPreference"; import { installMockBridge } from "../helpers/bridge"; import { passThroughBackupStep } from "../helpers/onboarding"; @@ -581,6 +582,42 @@ test("defaults auto-selects the only ready visible harness", async ({ await expect.poll(() => readSavedRuntime(page)).toBe("claude"); }); +test("defaults exclude device-disabled ready harnesses", async ({ page }) => { + await page.addInitScript((storageKey) => { + window.localStorage.setItem(storageKey, JSON.stringify(["claude"])); + }, ACP_RUNTIME_VISIBILITY_STORAGE_KEY); + await installMockBridge( + page, + { + acpRuntimesCatalog: [ + runtime("claude", "available", { status: "logged_in" }), + runtime("codex", "available", { status: "logged_in" }), + ], + globalAgentConfig: { + env_vars: {}, + provider: null, + model: null, + preferred_runtime: null, + }, + }, + { skipCommunitySeed: true, skipOnboardingSeed: true }, + ); + await page.goto("/"); + await navigateToSetupPage(page); + await page.getByTestId("onboarding-setup-next").click(); + await expect(page.getByTestId("onboarding-page-config")).toBeVisible(); + + const harness = page.getByTestId("global-agent-default-harness"); + await expect(harness).toHaveText("Codex"); + await harness.click(); + await expect( + page.getByTestId("global-agent-default-harness-option-claude"), + ).toHaveCount(0); + await expect( + page.getByTestId("global-agent-default-harness-option-codex"), + ).toBeVisible(); +}); + test("Finish waits for the latest rapid harness choice to persist", async ({ page, }) => {