Filter hidden harnesses from remaining pickers

Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-07-27 13:29:51 +01:00
parent 17329c4227
commit 6e81545168
7 changed files with 140 additions and 6 deletions
@@ -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(
@@ -95,6 +95,38 @@ export function filterEnabledAcpRuntimes<T extends { id: string }>(
);
}
/**
* Filter new configuration choices while preserving one explicitly saved
* runtime as a current-only option.
*/
export function runtimesForAcpConfigurationPicker<T extends { id: string }>(
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.
@@ -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(
() =>
@@ -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")),
@@ -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) =>
@@ -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 (
<div className="flex items-center gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
@@ -756,9 +779,12 @@ function RuntimeRow({
value={value}
>
<option value="">Default</option>
{providers.map((runtime) => (
{runtimeOptions.map((runtime) => (
<option key={runtime.id} value={runtime.id}>
{runtime.label}
{value === runtime.id && !selectableRuntimeIds.has(runtime.id)
? " (current)"
: ""}
</option>
))}
</select>
@@ -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,
}) => {