- No ACP runtimes found. Make sure an agent runtime (e.g. Goose)
- is installed.
+ {runtimes.length === 0
+ ? "No ACP runtimes found. Make sure an agent runtime (e.g. Goose) is installed."
+ : "No enabled fallback runtime is available. Turn on a harness to deploy runtime-less team members."}
) : null}
@@ -307,7 +316,7 @@ export function AddTeamToChannelDialog({
disabled={
!team ||
!selectedChannel ||
- !defaultProvider ||
+ !canResolveTeamRuntimes ||
resolved.length === 0 ||
missingPersonaCount > 0 ||
channelsQuery.isLoading ||
From 33d285e543e8df47069d0cba45b84772e0f0c8cd Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Thu, 23 Jul 2026 15:14:28 -0700
Subject: [PATCH 07/22] Preserve hidden harness defaults when editing
---
.../agents/ui/AddTeamToChannelDialog.tsx | 4 +--
.../agents/ui/useManagedAgentActions.ts | 4 +--
.../features/agents/useGlobalAgentConfig.ts | 35 +++++++++++++------
.../src/features/onboarding/welcomeKickoff.ts | 5 +--
.../features/profile/ui/UserProfilePanel.tsx | 4 +--
5 files changed, 34 insertions(+), 18 deletions(-)
diff --git a/desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx b/desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx
index 62475819a9..552373743f 100644
--- a/desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx
+++ b/desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx
@@ -5,7 +5,7 @@ import {
useAvailableAcpRuntimes,
useCreateChannelManagedAgentsMutation,
} from "@/features/agents/hooks";
-import { useGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig";
+import { useImplicitGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig";
import type { CreateChannelManagedAgentsResult } from "@/features/agents/channelAgents";
import {
emptyResolvedTeamPersonas,
@@ -54,7 +54,7 @@ export function AddTeamToChannelDialog({
onOpenChange,
onDeployed,
}: AddTeamToChannelDialogProps) {
- const { globalConfig } = useGlobalAgentConfig();
+ const { globalConfig } = useImplicitGlobalAgentConfig();
const channelsQuery = useChannelsQuery();
const providersQuery = useAvailableAcpRuntimes();
const [channelId, setChannelId] = React.useState("");
diff --git a/desktop/src/features/agents/ui/useManagedAgentActions.ts b/desktop/src/features/agents/ui/useManagedAgentActions.ts
index 4eb9b6ed03..31fff91cee 100644
--- a/desktop/src/features/agents/ui/useManagedAgentActions.ts
+++ b/desktop/src/features/agents/ui/useManagedAgentActions.ts
@@ -12,7 +12,7 @@ import {
useStopManagedAgentMutation,
useDeleteManagedAgentMutation,
} from "@/features/agents/hooks";
-import { useGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig";
+import { useImplicitGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig";
import { useChannelsQuery } from "@/features/channels/hooks";
import { usePresenceQuery } from "@/features/presence/hooks";
import type {
@@ -36,7 +36,7 @@ import {
} from "../lib/instanceInputForDefinition";
export function useManagedAgentActions() {
- const { globalConfig } = useGlobalAgentConfig();
+ const { globalConfig } = useImplicitGlobalAgentConfig();
const relayAgentsQuery = useRelayAgentsQuery();
const managedAgentsQuery = useManagedAgentsQuery();
const [shouldLoadChannels, setShouldLoadChannels] = React.useState(false);
diff --git a/desktop/src/features/agents/useGlobalAgentConfig.ts b/desktop/src/features/agents/useGlobalAgentConfig.ts
index 054bfb7c23..7b5e09191c 100644
--- a/desktop/src/features/agents/useGlobalAgentConfig.ts
+++ b/desktop/src/features/agents/useGlobalAgentConfig.ts
@@ -33,7 +33,6 @@ export function useGlobalAgentConfig(): {
globalConfig: GlobalAgentConfig;
isLoading: boolean;
} {
- const disabledRuntimeIds = useDisabledAcpRuntimeIds();
const { data, isPending } = useQuery({
queryKey: globalAgentConfigQueryKey,
queryFn: getGlobalAgentConfig,
@@ -43,17 +42,33 @@ export function useGlobalAgentConfig(): {
// Never show a stale empty flash while a background refetch runs.
placeholderData: EMPTY_CONFIG,
});
- const globalConfig = React.useMemo(
- () =>
- maskDisabledAcpRuntimePreference(
- data ?? EMPTY_CONFIG,
- disabledRuntimeIds,
- ),
- [data, disabledRuntimeIds],
- );
return {
- globalConfig,
+ globalConfig: data ?? EMPTY_CONFIG,
isLoading: isPending,
};
}
+
+/**
+ * Load global defaults for a new implicit runtime choice.
+ *
+ * Existing agent edit surfaces must use useGlobalAgentConfig so a hidden
+ * harness can still inherit its persisted provider and model. Start paths use
+ * this hook to ignore a hidden preferred harness and its dependent defaults.
+ */
+export function useImplicitGlobalAgentConfig(): {
+ globalConfig: GlobalAgentConfig;
+ isLoading: boolean;
+} {
+ const { globalConfig, isLoading } = useGlobalAgentConfig();
+ const disabledRuntimeIds = useDisabledAcpRuntimeIds();
+ const implicitGlobalConfig = React.useMemo(
+ () => maskDisabledAcpRuntimePreference(globalConfig, disabledRuntimeIds),
+ [disabledRuntimeIds, globalConfig],
+ );
+
+ return {
+ globalConfig: implicitGlobalConfig,
+ isLoading,
+ };
+}
diff --git a/desktop/src/features/onboarding/welcomeKickoff.ts b/desktop/src/features/onboarding/welcomeKickoff.ts
index fd5dac3947..5d40fbb858 100644
--- a/desktop/src/features/onboarding/welcomeKickoff.ts
+++ b/desktop/src/features/onboarding/welcomeKickoff.ts
@@ -5,7 +5,7 @@ import {
useAcpRuntimesQuery,
useManagedAgentsQuery,
} from "@/features/agents/hooks";
-import { useGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig";
+import { useImplicitGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig";
import { useCommunities } from "@/features/communities/useCommunities";
import { welcomeKickoffMarker } from "@/features/onboarding/devFreshOnboarding";
import { resolveAgentReadiness } from "@/features/onboarding/ui/agentReadiness";
@@ -493,7 +493,8 @@ export function useWelcomeKickoff(
const { activeCommunity } = useCommunities();
const runtimesQuery = useAcpRuntimesQuery();
const managedAgentsQuery = useManagedAgentsQuery();
- const { globalConfig, isLoading: configLoading } = useGlobalAgentConfig();
+ const { globalConfig, isLoading: configLoading } =
+ useImplicitGlobalAgentConfig();
const channelId = activeChannel?.id ?? null;
const isActiveWelcome = isWelcomeChannel(activeChannel);
const focusedWelcomeChannelRef = React.useRef(null);
diff --git a/desktop/src/features/profile/ui/UserProfilePanel.tsx b/desktop/src/features/profile/ui/UserProfilePanel.tsx
index c1f713d230..4dfdaaafc9 100644
--- a/desktop/src/features/profile/ui/UserProfilePanel.tsx
+++ b/desktop/src/features/profile/ui/UserProfilePanel.tsx
@@ -25,7 +25,7 @@ import {
useUpdateManagedAgentMutation,
useUpdatePersonaMutation,
} from "@/features/agents/hooks";
-import { useGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig";
+import { useImplicitGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig";
import { AddAgentToChannelDialog } from "@/features/agents/ui/AddAgentToChannelDialog";
import {
availableRuntimesForStart,
@@ -122,7 +122,7 @@ export function UserProfilePanel({
widthPx,
transparentChrome = false,
}: UserProfilePanelProps) {
- const { globalConfig } = useGlobalAgentConfig();
+ const { globalConfig } = useImplicitGlobalAgentConfig();
const isOverlay = useIsThreadPanelOverlay();
const isSplitLayout = layout === "split";
useEscapeKey(onClose, isOverlay || isSinglePanelView);
From 4e82da3615c82d8ca7d2ba1146104eb70e42b4a5 Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Thu, 23 Jul 2026 15:28:28 -0700
Subject: [PATCH 08/22] Filter implicit runtime fallbacks centrally
---
desktop/src/features/agents/AGENTS.md | 14 +++--
.../agents/lib/resolvePersonaRuntime.test.mjs | 40 +++++++++++++
.../agents/lib/resolvePersonaRuntime.ts | 60 ++++++++++++++++---
3 files changed, 99 insertions(+), 15 deletions(-)
diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md
index 23f645b9b7..99e2cdd79d 100644
--- a/desktop/src/features/agents/AGENTS.md
+++ b/desktop/src/features/agents/AGENTS.md
@@ -90,12 +90,14 @@ with a TypeScript lookup table or an id comparison in a component.
editor persists its visible fallback on the next save. Its dependent
provider/model defaults are also ignored for new implicit fallback agents,
without changing the persisted configuration used by existing agents.
- `resolveStartRuntimeForDefinition` is the shared boundary that filters every
- runtime-less start through the device's visible-runtime set; call it instead
- of duplicating default selection in a start surface. Team deploys use the
- same visible-runtime set only for members that need an implicit fallback, so
- a fully pinned team remains deployable even when every installed runtime is
- hidden. Definitions already pinned to a hidden runtime remain runnable.
+ `resolvePersonaRuntime` is the shared visibility boundary for every
+ runtime-less deployment or provisioning path. Definition-to-instance starts
+ use the stricter `resolveStartRuntimeForDefinition` wrapper; call one of
+ these shared resolvers instead of duplicating default selection in a start
+ surface. Team deploys use the same visible-runtime set only for members that
+ need an implicit fallback, so a fully pinned team remains deployable even
+ when every installed runtime is hidden. Definitions already pinned to a
+ hidden runtime remain runnable.
## The tests that enforce this
diff --git a/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs b/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs
index f77f056d64..6a5b66a5e5 100644
--- a/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs
+++ b/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs
@@ -33,6 +33,46 @@ test("resolvePersonaRuntime — undefined personaRuntimeId also returns defaultR
});
});
+test("resolvePersonaRuntime — hidden defaults are skipped for runtime-less personas", () => {
+ const result = resolvePersonaRuntime(null, runtimes, goose, false, ["goose"]);
+ assert.deepEqual(result, {
+ runtime: claude,
+ warnings: [],
+ isOverridden: false,
+ });
+});
+
+test("resolvePersonaRuntime — explicitly pinned hidden runtimes remain available", () => {
+ const result = resolvePersonaRuntime("goose", runtimes, claude, false, [
+ "goose",
+ ]);
+ assert.deepEqual(result, {
+ runtime: goose,
+ warnings: [],
+ isOverridden: false,
+ });
+});
+
+test("resolvePersonaRuntime — hidden fallback is replaced when a pinned runtime is unavailable", () => {
+ const result = resolvePersonaRuntime("unknown-rt", runtimes, goose, false, [
+ "goose",
+ ]);
+ assert.equal(result.runtime, claude);
+ assert.equal(result.warnings.length, 1);
+ assert.match(result.warnings[0], /Claude/);
+ assert.equal(result.isOverridden, true);
+});
+
+test("resolvePersonaRuntime — no implicit fallback remains when every runtime is hidden", () => {
+ const result = resolvePersonaRuntime(null, runtimes, goose, false, [
+ "goose",
+ "claude",
+ ]);
+ assert.equal(result.runtime, null);
+ assert.equal(result.warnings.length, 1);
+ assert.equal(result.isOverridden, false);
+});
+
test("resolvePersonaRuntime — no personaRuntimeId and no defaultRuntime returns null with warning", () => {
const result = resolvePersonaRuntime(null, runtimes, null);
assert.equal(result.runtime, null);
diff --git a/desktop/src/features/agents/lib/resolvePersonaRuntime.ts b/desktop/src/features/agents/lib/resolvePersonaRuntime.ts
index 474c218f66..3b09e5b4f7 100644
--- a/desktop/src/features/agents/lib/resolvePersonaRuntime.ts
+++ b/desktop/src/features/agents/lib/resolvePersonaRuntime.ts
@@ -1,4 +1,8 @@
import type { AcpRuntime, AcpRuntimeCatalogEntry } from "@/shared/api/types";
+import {
+ filterEnabledAcpRuntimes,
+ getDisabledAcpRuntimeIdsSnapshot,
+} from "./runtimeVisibilityPreference";
/**
* Select the best default runtime from a catalog, using the same preference
@@ -55,18 +59,31 @@ export type ResolvePersonaRuntimeResult = {
* fall back to `defaultRuntime` and emit a warning.
* 4. If there is no `defaultRuntime` either → return `null` with an error
* warning so the UI can block deployment.
+ *
+ * Hidden runtimes remain eligible when explicitly pinned by a persona. They
+ * are removed only from the implicit fallback set, at this shared boundary,
+ * so every provisioning surface observes the device visibility preference.
*/
export function resolvePersonaRuntime(
personaRuntimeId: string | undefined | null,
runtimes: readonly AcpRuntime[],
defaultRuntime: AcpRuntime | null,
forceOverride?: boolean,
+ disabledRuntimeIds: readonly string[] = getDisabledAcpRuntimeIdsSnapshot(),
): ResolvePersonaRuntimeResult {
+ const implicitDefaultRuntime = forceOverride
+ ? defaultRuntime
+ : resolveVisibleDefaultRuntime(
+ runtimes,
+ defaultRuntime,
+ disabledRuntimeIds,
+ );
+
// Case 1: Persona has no runtime preference — use the default.
if (!personaRuntimeId) {
return {
- runtime: defaultRuntime,
- warnings: defaultRuntime
+ runtime: implicitDefaultRuntime,
+ warnings: implicitDefaultRuntime
? []
: [
"No agent runtimes are available. Install a runtime (e.g. Goose) to deploy agents.",
@@ -78,28 +95,35 @@ export function resolvePersonaRuntime(
// Case 2: Persona's preferred runtime is available.
const matched = runtimes.find((p) => p.id === personaRuntimeId);
if (matched) {
- if (forceOverride && defaultRuntime && matched.id !== defaultRuntime.id) {
+ if (
+ forceOverride &&
+ implicitDefaultRuntime &&
+ matched.id !== implicitDefaultRuntime.id
+ ) {
return {
- runtime: defaultRuntime,
+ runtime: implicitDefaultRuntime,
warnings: [
- `Runtime override: using ${defaultRuntime.label} instead of ${matched.label}.`,
+ `Runtime override: using ${implicitDefaultRuntime.label} instead of ${matched.label}.`,
],
isOverridden: true,
};
}
return {
- runtime: forceOverride && defaultRuntime ? defaultRuntime : matched,
+ runtime:
+ forceOverride && implicitDefaultRuntime
+ ? implicitDefaultRuntime
+ : matched,
warnings: [],
isOverridden: false,
};
}
// Case 3 & 4: Persona's runtime is not available — fall back.
- if (defaultRuntime) {
+ if (implicitDefaultRuntime) {
return {
- runtime: defaultRuntime,
+ runtime: implicitDefaultRuntime,
warnings: [
- `This agent is configured for runtime "${personaRuntimeId}" but it is not available. Using ${defaultRuntime.label} instead.`,
+ `This agent is configured for runtime "${personaRuntimeId}" but it is not available. Using ${implicitDefaultRuntime.label} instead.`,
],
isOverridden: true,
};
@@ -114,6 +138,24 @@ export function resolvePersonaRuntime(
};
}
+function resolveVisibleDefaultRuntime(
+ runtimes: readonly AcpRuntime[],
+ defaultRuntime: AcpRuntime | null,
+ disabledRuntimeIds: readonly string[],
+): AcpRuntime | null {
+ if (!defaultRuntime) return null;
+
+ const visibleRuntimes = filterEnabledAcpRuntimes(
+ runtimes,
+ disabledRuntimeIds,
+ );
+ return (
+ visibleRuntimes.find((runtime) => runtime.id === defaultRuntime.id) ??
+ visibleRuntimes[0] ??
+ null
+ );
+}
+
/**
* Collect runtime-resolution warnings for a list of personas.
*
From 93cf670e77787f2f1d27d4eef552c2cffcb39f41 Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Thu, 23 Jul 2026 15:42:01 -0700
Subject: [PATCH 09/22] Scope hidden harness defaults by dialog mode
---
desktop/src/features/agents/AGENTS.md | 2 +
.../agents/ui/AgentDefinitionDialog.tsx | 4 +-
.../agents/ui/AgentInstanceEditDialog.tsx | 6 ++-
.../agents/ui/agentConfigOptions.test.mjs | 14 +++++
.../features/agents/ui/agentConfigOptions.tsx | 18 ++++---
.../agents/ui/useAgentDialogDefaults.test.mjs | 42 +++++++++++++++
.../agents/ui/useAgentDialogDefaults.ts | 51 ++++++++++++++++++-
7 files changed, 126 insertions(+), 11 deletions(-)
create mode 100644 desktop/src/features/agents/ui/useAgentDialogDefaults.test.mjs
diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md
index 99e2cdd79d..544981cb91 100644
--- a/desktop/src/features/agents/AGENTS.md
+++ b/desktop/src/features/agents/AGENTS.md
@@ -90,6 +90,8 @@ with a TypeScript lookup table or an id comparison in a component.
editor persists its visible fallback on the next save. Its dependent
provider/model defaults are also ignored for new implicit fallback agents,
without changing the persisted configuration used by existing agents.
+ Create-mode dialogs use the implicit masked config; existing definition and
+ instance edit dialogs use the raw persisted config.
`resolvePersonaRuntime` is the shared visibility boundary for every
runtime-less deployment or provisioning path. Definition-to-instance starts
use the stricter `resolveStartRuntimeForDefinition` wrapper; call one of
diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
index f496496b8d..b054409ade 100644
--- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
@@ -68,7 +68,7 @@ import {
usePersonaModelDiscovery,
} from "./usePersonaModelDiscovery";
import { useBakedBuildEnvKeysQuery, useRuntimeFileConfigQuery } from "../hooks";
-import { useAgentDialogDefaults } from "./useAgentDialogDefaults";
+import { useDefinitionAgentDialogDefaults } from "./useAgentDialogDefaults";
import { AgentAiDefaultsNotice } from "./AgentAiDefaults";
import { AgentDefaultsDialog } from "./AgentDefaultsDialog";
import {
@@ -164,7 +164,7 @@ export function AgentDefinitionDialog({
model: inheritedModelDefault,
},
inheritedEnvVars: inheritedEnvVarsForAdvanced,
- } = useAgentDialogDefaults({ open });
+ } = useDefinitionAgentDialogDefaults(initialValues, open);
const selectableRuntimes = useSelectableAcpRuntimes(runtimes);
const defaultRuntime = getDefaultPersonaRuntime(
selectableRuntimes,
diff --git a/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx b/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
index fa115387a9..ca7731b18d 100644
--- a/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
@@ -366,7 +366,11 @@ export function AgentInstanceEditDialog({
model: inheritedModelDefault,
},
inheritedEnvVars: inheritedEnvVarsForAdvanced,
- } = useAgentDialogDefaults({ inheritedEnvVars, open });
+ } = useAgentDialogDefaults({
+ configScope: "existing",
+ inheritedEnvVars,
+ open,
+ });
// Runtime/provider-required credential state, derived from the PROSPECTIVE
// post-submit runtime — see the hook for the inherit-transition and
diff --git a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
index ffa680006d..ed71f6477d 100644
--- a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
+++ b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
@@ -207,6 +207,20 @@ test("reconcilePreferredRuntimeFallback updates a hidden saved default", () => {
assert.equal(reconcilePreferredRuntimeFallback(config, "goose"), config);
});
+test("reconcilePreferredRuntimeFallback persists an unsaved displayed fallback", () => {
+ const config = {
+ env_vars: { SHARED: "kept" },
+ provider: "relay-mesh",
+ model: "auto",
+ preferred_runtime: null,
+ };
+
+ assert.deepEqual(reconcilePreferredRuntimeFallback(config, "buzz-agent"), {
+ ...config,
+ preferred_runtime: "buzz-agent",
+ });
+});
+
// ── getPersonaModelOptions — codex/claude do not use global provider ──────────
//
// The discovery call in AgentDefinitionDialog passes
diff --git a/desktop/src/features/agents/ui/agentConfigOptions.tsx b/desktop/src/features/agents/ui/agentConfigOptions.tsx
index 120098c16e..2eecb36715 100644
--- a/desktop/src/features/agents/ui/agentConfigOptions.tsx
+++ b/desktop/src/features/agents/ui/agentConfigOptions.tsx
@@ -199,21 +199,25 @@ export function resetConfigForHarnessChange(
}
/**
- * Align a stale or hidden saved preference with the fallback shown by a
- * runtime selector, clearing values that are not portable across harnesses.
+ * Align a missing, stale, or hidden saved preference with the shown fallback.
+ * A missing preference adopts the current context without clearing its draft;
+ * switching away from a saved harness clears values that are not portable.
*/
export function reconcilePreferredRuntimeFallback(
config: GlobalAgentConfig,
fallbackRuntimeId: string | null,
): GlobalAgentConfig {
- if (
- !config.preferred_runtime ||
- !fallbackRuntimeId ||
- config.preferred_runtime === fallbackRuntimeId
- ) {
+ if (!fallbackRuntimeId || config.preferred_runtime === fallbackRuntimeId) {
return config;
}
+ if (!config.preferred_runtime) {
+ return {
+ ...config,
+ preferred_runtime: fallbackRuntimeId,
+ };
+ }
+
return resetConfigForHarnessChange(config, fallbackRuntimeId);
}
diff --git a/desktop/src/features/agents/ui/useAgentDialogDefaults.test.mjs b/desktop/src/features/agents/ui/useAgentDialogDefaults.test.mjs
new file mode 100644
index 0000000000..12e0a02b2d
--- /dev/null
+++ b/desktop/src/features/agents/ui/useAgentDialogDefaults.test.mjs
@@ -0,0 +1,42 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+ agentDefinitionConfigScope,
+ resolveAgentDialogGlobalConfig,
+} from "./useAgentDialogDefaults.ts";
+
+const persistedConfig = {
+ env_vars: { SHARED: "kept" },
+ provider: "relay-mesh",
+ model: "auto",
+ preferred_runtime: "buzz-agent",
+};
+
+test("create-mode defaults mask values owned by a hidden harness", () => {
+ assert.deepEqual(
+ resolveAgentDialogGlobalConfig(persistedConfig, "implicit", ["buzz-agent"]),
+ {
+ env_vars: { SHARED: "kept" },
+ provider: null,
+ model: null,
+ preferred_runtime: null,
+ },
+ );
+});
+
+test("existing edit defaults preserve values owned by a hidden harness", () => {
+ assert.equal(
+ resolveAgentDialogGlobalConfig(persistedConfig, "existing", ["buzz-agent"]),
+ persistedConfig,
+ );
+});
+
+test("definition dialogs select config scope from create versus edit values", () => {
+ assert.equal(agentDefinitionConfigScope(null), "implicit");
+ assert.equal(agentDefinitionConfigScope({ displayName: "New" }), "implicit");
+ assert.equal(
+ agentDefinitionConfigScope({ id: "existing", displayName: "Existing" }),
+ "existing",
+ );
+});
diff --git a/desktop/src/features/agents/ui/useAgentDialogDefaults.ts b/desktop/src/features/agents/ui/useAgentDialogDefaults.ts
index 5ede9558fe..f9fbbd31c7 100644
--- a/desktop/src/features/agents/ui/useAgentDialogDefaults.ts
+++ b/desktop/src/features/agents/ui/useAgentDialogDefaults.ts
@@ -1,18 +1,57 @@
import * as React from "react";
+import type {
+ CreatePersonaInput,
+ GlobalAgentConfig,
+ UpdatePersonaInput,
+} from "@/shared/api/types";
import { useBakedBuildEnvQuery } from "../hooks";
+import {
+ maskDisabledAcpRuntimePreference,
+ useDisabledAcpRuntimeIds,
+} from "../lib/runtimeVisibilityPreference";
import { useGlobalAgentConfig } from "../useGlobalAgentConfig";
import { BUZZ_AGENT_THINKING_EFFORT } from "./buzzAgentConfig";
import { getInheritedAgentDefaults } from "./bakedEnvHelpers";
+export type AgentDialogConfigScope = "existing" | "implicit";
+
+export function agentDefinitionConfigScope(
+ initialValues: CreatePersonaInput | UpdatePersonaInput | null,
+): AgentDialogConfigScope {
+ return initialValues && "id" in initialValues ? "existing" : "implicit";
+}
+
+export function resolveAgentDialogGlobalConfig(
+ persistedConfig: GlobalAgentConfig,
+ configScope: AgentDialogConfigScope,
+ disabledRuntimeIds: readonly string[],
+): GlobalAgentConfig {
+ return configScope === "implicit"
+ ? maskDisabledAcpRuntimePreference(persistedConfig, disabledRuntimeIds)
+ : persistedConfig;
+}
+
export function useAgentDialogDefaults({
+ configScope,
inheritedEnvVars = {},
open,
}: {
+ configScope: AgentDialogConfigScope;
inheritedEnvVars?: Record;
open: boolean;
}) {
- const { globalConfig } = useGlobalAgentConfig();
+ const { globalConfig: persistedConfig } = useGlobalAgentConfig();
+ const disabledRuntimeIds = useDisabledAcpRuntimeIds();
+ const globalConfig = React.useMemo(
+ () =>
+ resolveAgentDialogGlobalConfig(
+ persistedConfig,
+ configScope,
+ disabledRuntimeIds,
+ ),
+ [configScope, disabledRuntimeIds, persistedConfig],
+ );
const { data: bakedEnv } = useBakedBuildEnvQuery({ enabled: open });
const inheritedDefaults = getInheritedAgentDefaults(globalConfig, bakedEnv);
const effectiveInheritedEnvVars = React.useMemo(
@@ -31,3 +70,13 @@ export function useAgentDialogDefaults({
inheritedEnvVars: effectiveInheritedEnvVars,
};
}
+
+export function useDefinitionAgentDialogDefaults(
+ initialValues: CreatePersonaInput | UpdatePersonaInput | null,
+ open: boolean,
+) {
+ return useAgentDialogDefaults({
+ configScope: agentDefinitionConfigScope(initialValues),
+ open,
+ });
+}
From d9b164fbf4ee0b6b78e1c2f60a2d1b744e04e2a9 Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Thu, 23 Jul 2026 15:58:37 -0700
Subject: [PATCH 10/22] Fix hidden agent harness fallbacks
---
.../agents/lib/resolvePersonaRuntime.test.mjs | 14 +++++++++-
.../agents/lib/resolvePersonaRuntime.ts | 6 +----
.../agents/ui/AgentDefinitionDialog.tsx | 19 +++++--------
.../agents/ui/AgentInstanceEditDialog.tsx | 17 ++++++------
.../agents/ui/agentConfigOptions.test.mjs | 27 +++++++++++++++++++
.../features/agents/ui/agentConfigOptions.tsx | 27 ++++++++++++++++---
6 files changed, 79 insertions(+), 31 deletions(-)
diff --git a/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs b/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs
index 6a5b66a5e5..89991a705d 100644
--- a/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs
+++ b/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs
@@ -8,9 +8,10 @@ import {
} from "./resolvePersonaRuntime.ts";
function makeRuntime(id, label = `${id} label`) {
- return { id, label, command: id, avatarUrl: "" };
+ return { id, label, command: id, avatarUrl: "", availability: "available" };
}
+const buzzAgent = makeRuntime("buzz-agent", "Buzz Agent");
const goose = makeRuntime("goose", "Goose");
const claude = makeRuntime("claude", "Claude");
const runtimes = [goose, claude];
@@ -42,6 +43,17 @@ test("resolvePersonaRuntime — hidden defaults are skipped for runtime-less per
});
});
+test("resolvePersonaRuntime — hidden defaults preserve product fallback order", () => {
+ const result = resolvePersonaRuntime(
+ null,
+ [goose, claude, buzzAgent],
+ goose,
+ false,
+ ["goose"],
+ );
+ assert.equal(result.runtime, buzzAgent);
+});
+
test("resolvePersonaRuntime — explicitly pinned hidden runtimes remain available", () => {
const result = resolvePersonaRuntime("goose", runtimes, claude, false, [
"goose",
diff --git a/desktop/src/features/agents/lib/resolvePersonaRuntime.ts b/desktop/src/features/agents/lib/resolvePersonaRuntime.ts
index 3b09e5b4f7..69f484d1eb 100644
--- a/desktop/src/features/agents/lib/resolvePersonaRuntime.ts
+++ b/desktop/src/features/agents/lib/resolvePersonaRuntime.ts
@@ -149,11 +149,7 @@ function resolveVisibleDefaultRuntime(
runtimes,
disabledRuntimeIds,
);
- return (
- visibleRuntimes.find((runtime) => runtime.id === defaultRuntime.id) ??
- visibleRuntimes[0] ??
- null
- );
+ return getDefaultPersonaRuntime(visibleRuntimes, defaultRuntime.id);
}
/**
diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
index b054409ade..c690b8032b 100644
--- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
@@ -35,11 +35,11 @@ import { personaSubmitBlock } from "./personaSubmitBlock";
import {
AUTO_MODEL_DROPDOWN_VALUE,
AUTO_PROVIDER_DROPDOWN_VALUE,
- BLOCK_BUILD_HIDDEN_PROVIDER_IDS,
CUSTOM_PROVIDER_DROPDOWN_VALUE,
computeLocalModeGate,
formatRuntimeOptionLabel,
getDefaultPersonaRuntime,
+ getPersonaHiddenProviderIds,
getPersonaModelOptions,
getPersonaProviderOptions,
getRuntimePersonaModelOptions,
@@ -527,17 +527,12 @@ export function AgentDefinitionDialog({
modelFieldVisible,
provider: effectiveProvider,
});
- // On internal Block builds, BUZZ_AGENT_PROVIDER is baked in and a boot
- // migration rewrites any persisted Databricks v1 values → v2. Hide the v1
- // option there so it is not offered for new selections. OSS builds have no
- // baked provider, so v1 remains visible.
- const hideProviderIds = React.useMemo(
- () =>
- (bakedEnvKeys ?? []).includes("BUZZ_AGENT_PROVIDER")
- ? BLOCK_BUILD_HIDDEN_PROVIDER_IDS
- : new Set(),
- [bakedEnvKeys],
- );
+ const hideProviderIds = getPersonaHiddenProviderIds({
+ bakedEnvKeys: bakedEnvKeys ?? [],
+ selectableRuntimes,
+ currentRuntimeId: runtime,
+ preserveCurrentRuntime: !isCreateMode,
+ });
const providerOptions = getPersonaProviderOptions(
trimmedProvider,
runtime,
diff --git a/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx b/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
index ca7731b18d..9a54f987be 100644
--- a/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
@@ -28,12 +28,13 @@ import { setManagedAgentAutoRestart } from "@/shared/api/tauriManagedAgents";
import { EditAgentAdvancedFields } from "./EditAgentAdvancedFields";
import {
AUTO_PROVIDER_DROPDOWN_VALUE,
- BLOCK_BUILD_HIDDEN_PROVIDER_IDS,
CUSTOM_PROVIDER_DROPDOWN_VALUE,
formatRuntimeOptionLabel,
getDefaultLlmModelLabel,
getDefaultPersonaRuntime,
+ getPersonaHiddenProviderIds,
getPersonaProviderOptions,
+ getProviderApiKeyEnvVar,
isMissingRequiredDropdownField,
NO_RUNTIME_DROPDOWN_VALUE,
PERSONA_FIELD_CONTROL_CLASS,
@@ -76,7 +77,6 @@ import {
getBakedModelInheritLabel,
getBakedProviderInheritLabel,
} from "./bakedEnvHelpers";
-import { getProviderApiKeyEnvVar } from "./agentConfigOptions";
import { useAgentDialogDefaults } from "./useAgentDialogDefaults";
import { AgentAiDefaultsNotice } from "./AgentAiDefaults";
import { AgentDefaultsDialog } from "./AgentDefaultsDialog";
@@ -797,13 +797,12 @@ export function AgentInstanceEditDialog({
// Provider field derived state
const trimmedProvider = provider.trim();
- const hideProviderIds = React.useMemo(
- () =>
- (bakedEnvKeys ?? []).includes("BUZZ_AGENT_PROVIDER")
- ? BLOCK_BUILD_HIDDEN_PROVIDER_IDS
- : new Set(),
- [bakedEnvKeys],
- );
+ const hideProviderIds = getPersonaHiddenProviderIds({
+ bakedEnvKeys: bakedEnvKeys ?? [],
+ selectableRuntimes,
+ currentRuntimeId: selectedRuntimeId,
+ preserveCurrentRuntime: true,
+ });
const providerOptions = getPersonaProviderOptions(
trimmedProvider,
selectedRuntime?.id ?? "",
diff --git a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
index ed71f6477d..ae078f2fc2 100644
--- a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
+++ b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
@@ -3,6 +3,7 @@ import test from "node:test";
import {
getDefaultPersonaRuntime,
+ getPersonaHiddenProviderIds,
getPersonaModelOptions,
getPersonaProviderOptions,
reconcilePreferredRuntimeFallback,
@@ -75,6 +76,32 @@ test("getPersonaProviderOptions appends (current) tail for an unknown saved prov
assert.equal(tail?.label, "my-custom-llm (current)");
});
+test("hidden Buzz Agent suppresses shared compute for new selections", () => {
+ const hidden = getPersonaHiddenProviderIds({
+ bakedEnvKeys: [],
+ selectableRuntimes: [makeRuntime("goose")],
+ currentRuntimeId: "goose",
+ preserveCurrentRuntime: false,
+ });
+ const ids = getPersonaProviderOptions("", "goose", "", hidden).map(
+ (option) => option.id,
+ );
+ assert.ok(!ids.includes("relay-mesh"));
+});
+
+test("an existing hidden Buzz Agent keeps its shared compute provider", () => {
+ const hidden = getPersonaHiddenProviderIds({
+ bakedEnvKeys: [],
+ selectableRuntimes: [makeRuntime("goose")],
+ currentRuntimeId: "buzz-agent",
+ preserveCurrentRuntime: true,
+ });
+ const ids = getPersonaProviderOptions("", "buzz-agent", "", hidden).map(
+ (option) => option.id,
+ );
+ assert.ok(ids.includes("relay-mesh"));
+});
+
// ── getDefaultPersonaRuntime — buzz-agent first ───────────────────────────────
test("getDefaultPersonaRuntime honors an available global preference", () => {
diff --git a/desktop/src/features/agents/ui/agentConfigOptions.tsx b/desktop/src/features/agents/ui/agentConfigOptions.tsx
index 2eecb36715..fda3825e11 100644
--- a/desktop/src/features/agents/ui/agentConfigOptions.tsx
+++ b/desktop/src/features/agents/ui/agentConfigOptions.tsx
@@ -14,15 +14,34 @@ export { getDefaultPersonaRuntime } from "../lib/resolvePersonaRuntime";
* offering it for new selections would create a regression path.
* OSS builds pass an empty `Set` so v1 remains visible.
*
- * All three dialog sites that show a provider picker import this constant —
- * `AgentDefinitionDialog`, `AgentInstanceEditDialog`, and
- * `AgentDefaultsSettingsCard` — making it the single source of truth for
- * which provider ids to suppress on Block builds.
+ * Provider pickers consume this directly or through
+ * `getPersonaHiddenProviderIds`, keeping one source of truth for Block builds.
*/
export const BLOCK_BUILD_HIDDEN_PROVIDER_IDS: ReadonlySet = new Set([
"databricks",
]);
+export function getPersonaHiddenProviderIds({
+ bakedEnvKeys,
+ selectableRuntimes,
+ currentRuntimeId,
+ preserveCurrentRuntime,
+}: {
+ bakedEnvKeys: readonly string[];
+ selectableRuntimes: readonly Pick[];
+ currentRuntimeId: string;
+ preserveCurrentRuntime: boolean;
+}): ReadonlySet {
+ const hidden = bakedEnvKeys.includes("BUZZ_AGENT_PROVIDER")
+ ? new Set(BLOCK_BUILD_HIDDEN_PROVIDER_IDS)
+ : new Set();
+ const buzzAgentSelectable =
+ selectableRuntimes.some((runtime) => runtime.id === "buzz-agent") ||
+ (preserveCurrentRuntime && currentRuntimeId.trim() === "buzz-agent");
+ if (!buzzAgentSelectable) hidden.add("relay-mesh");
+ return hidden;
+}
+
export const PERSONA_FIELD_SHELL_CLASS =
"rounded-xl border border-input bg-muted/40 transition-colors duration-150 ease-out hover:border-muted-foreground/40 focus-within:border-muted-foreground/50";
export const PERSONA_FIELD_CONTROL_CLASS =
From 642fedcff099748bc138e6ba2a312cb05f50121c Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Thu, 23 Jul 2026 16:29:59 -0700
Subject: [PATCH 11/22] Block hidden harness provisioning bypasses
---
.../agents/lib/resolvePersonaRuntime.test.mjs | 17 ++++
.../agents/lib/resolvePersonaRuntime.ts | 10 ++-
.../lib/runtimeVisibilityPreference.test.mjs | 14 +++
.../agents/lib/runtimeVisibilityPreference.ts | 18 ++++
.../agents/ui/AgentDefinitionDialog.tsx | 86 ++++++++++---------
.../channel-templates/useApplyTemplate.ts | 6 +-
.../channels/ui/AddChannelBotDialog.tsx | 46 +++++++---
7 files changed, 138 insertions(+), 59 deletions(-)
diff --git a/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs b/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs
index 89991a705d..ffe75d69b3 100644
--- a/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs
+++ b/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs
@@ -252,3 +252,20 @@ test("team resolution requires a fallback for runtime-less definitions", () => {
true,
);
});
+
+test("team resolution blocks runtime-less definitions when all fallbacks are hidden", () => {
+ assert.equal(
+ canResolveAllPersonaRuntimes([{ runtime: null }], runtimes, goose, [
+ "goose",
+ "claude",
+ ]),
+ false,
+ );
+ assert.equal(
+ canResolveAllPersonaRuntimes([{ runtime: "goose" }], runtimes, goose, [
+ "goose",
+ "claude",
+ ]),
+ true,
+ );
+});
diff --git a/desktop/src/features/agents/lib/resolvePersonaRuntime.ts b/desktop/src/features/agents/lib/resolvePersonaRuntime.ts
index 69f484d1eb..6e1b9977aa 100644
--- a/desktop/src/features/agents/lib/resolvePersonaRuntime.ts
+++ b/desktop/src/features/agents/lib/resolvePersonaRuntime.ts
@@ -186,10 +186,16 @@ export function canResolveAllPersonaRuntimes(
personas: readonly { runtime: string | null }[],
runtimes: readonly AcpRuntime[],
fallbackRuntime: AcpRuntime | null,
+ disabledRuntimeIds: readonly string[] = getDisabledAcpRuntimeIdsSnapshot(),
): boolean {
return personas.every(
(persona) =>
- resolvePersonaRuntime(persona.runtime, runtimes, fallbackRuntime)
- .runtime !== null,
+ resolvePersonaRuntime(
+ persona.runtime,
+ runtimes,
+ fallbackRuntime,
+ false,
+ disabledRuntimeIds,
+ ).runtime !== null,
);
}
diff --git a/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs b/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs
index 662fbeb2d0..7230d49424 100644
--- a/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs
+++ b/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs
@@ -9,6 +9,7 @@ import {
parseDisabledAcpRuntimeIds,
readDisabledAcpRuntimeIds,
runtimesForImplicitAcpSelection,
+ visibleAcpRuntimeSeedForCreate,
} from "./runtimeVisibilityPreference.ts";
test("runtime visibility parsing is normalized and corruption tolerant", () => {
@@ -55,6 +56,19 @@ test("disabled runtimes are removed from selectable catalog entries", () => {
);
});
+test("create seeds replace hidden runtimes but preserve selectable ones", () => {
+ const runtimes = [{ id: "buzz-agent" }, { id: "goose" }];
+ assert.equal(
+ visibleAcpRuntimeSeedForCreate("claude", runtimes, "buzz-agent"),
+ "buzz-agent",
+ );
+ assert.equal(visibleAcpRuntimeSeedForCreate("claude", [], null), "");
+ assert.equal(
+ visibleAcpRuntimeSeedForCreate("goose", runtimes, "buzz-agent"),
+ "goose",
+ );
+});
+
test("stored runtime visibility is read from the versioned device key", () => {
const storage = {
getItem(key) {
diff --git a/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts b/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts
index 071176ead1..89932e6030 100644
--- a/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts
+++ b/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts
@@ -109,6 +109,24 @@ export function runtimesForImplicitAcpSelection(
: filterEnabledAcpRuntimes(runtimes, disabledRuntimeIds);
}
+/** Replace a hidden create-mode seed without changing edit-mode behavior. */
+export function visibleAcpRuntimeSeedForCreate(
+ runtimeId: string,
+ selectableRuntimes: readonly T[],
+ fallbackRuntimeId: string | null | undefined,
+): string {
+ const normalizedRuntimeId = normalizeRuntimeId(runtimeId);
+ if (
+ !normalizedRuntimeId ||
+ selectableRuntimes.some(
+ (runtime) => normalizeRuntimeId(runtime.id) === normalizedRuntimeId,
+ )
+ ) {
+ return runtimeId.trim();
+ }
+ return fallbackRuntimeId?.trim() ?? "";
+}
+
/**
* Prevent a disabled runtime and its dependent defaults from remaining
* effective for new implicit selections.
diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
index c690b8032b..bb2ef3ff90 100644
--- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
@@ -83,7 +83,10 @@ import {
} from "./agentAiConfigurationPolicy";
import { useProviderApiKeyFieldState } from "./providerApiKeyFieldState";
import { buildRuntimeModelProviderPayload } from "./agentDefinitionSubmitPayload";
-import { useSelectableAcpRuntimes } from "../lib/runtimeVisibilityPreference";
+import {
+ useSelectableAcpRuntimes,
+ visibleAcpRuntimeSeedForCreate,
+} from "../lib/runtimeVisibilityPreference";
type AgentDefinitionDialogProps = {
open: boolean;
@@ -143,16 +146,11 @@ export function AgentDefinitionDialog({
const [behaviorDraft, setBehaviorDraft] = React.useState(
emptyPersonaBehaviorDraft,
);
- // The seed the draft is diffed against at submit: an untouched quad
- // submits no behavior group, keeping unrelated edits hash-quiet.
+ // Untouched behavior fields submit no group, keeping edits hash-quiet.
const behaviorSeedRef = React.useRef(emptyPersonaBehaviorDraft);
- // Tracks when the runtime was auto-seeded by the default-runtime effect in
- // edit mode (i.e. the user never explicitly chose a runtime). Used to omit
- // the seeded runtime from the submit payload for builtin definitions whose
- // canonical runtime is null — the sync would revert it anyway.
+ // Lets edit-mode builtin definitions omit an untouched auto-seeded runtime.
const isRuntimeAutoSeededRef = React.useRef(false);
- // Seed once per open so choosing "No preference" cannot snap the dropdown
- // back to the default.
+ // Prevent "No preference" from snapping back to the default.
const hasSeededForOpenRef = React.useRef(false);
const [showAdvancedFields, setShowAdvancedFields] = React.useState(false);
const [isAvatarUploadPending, setIsAvatarUploadPending] =
@@ -208,15 +206,44 @@ export function AgentDefinitionDialog({
setBehaviorDraft(nextBehaviorDraft);
setNamePoolText(nextNamePoolText);
setEnvVars(nextEnvVars);
- // Item 5: collapsed by default in edit mode — only expand if a non-default
- // behavior value demands attention. Having env vars or a name pool is not
- // sufficient reason to auto-open.
setShowAdvancedFields(false);
setIsAvatarUploadPending(false);
isRuntimeAutoSeededRef.current = false;
hasSeededForOpenRef.current = false;
}, [initialValues, open]);
+ React.useEffect(() => {
+ if (!open || !initialValues || "id" in initialValues || runtimesLoading) {
+ return;
+ }
+ const seededRuntime = initialValues.runtime?.trim() ?? "";
+ const nextRuntime = visibleAcpRuntimeSeedForCreate(
+ seededRuntime,
+ selectableRuntimes,
+ defaultRuntime?.id,
+ );
+ if (
+ !seededRuntime ||
+ runtime.trim() !== seededRuntime ||
+ nextRuntime === seededRuntime
+ ) {
+ return;
+ }
+ setRuntime(nextRuntime);
+ setModel("");
+ setProvider("");
+ setAiConfigurationMode("defaults");
+ setIsCustomModelEditing(false);
+ setIsCustomProviderEditing(false);
+ }, [
+ defaultRuntime?.id,
+ initialValues,
+ open,
+ runtime,
+ runtimesLoading,
+ selectableRuntimes,
+ ]);
+
React.useEffect(() => {
if (
!open ||
@@ -233,10 +260,7 @@ export function AgentDefinitionDialog({
setRuntime(defaultRuntime.id);
hasSeededForOpenRef.current = true;
if ("id" in initialValues) {
- // Edit mode: record that this runtime was auto-seeded so the submit path
- // can omit it from the payload for builtin definitions (canonical runtime
- // null; sync would revert the value anyway). Explicit user changes via
- // the dropdown clear this flag.
+ // Builtin definitions omit this untouched inferred runtime on submit.
isRuntimeAutoSeededRef.current = true;
}
}, [defaultRuntime, initialValues, open, runtime, runtimesLoading]);
@@ -258,16 +282,14 @@ export function AgentDefinitionDialog({
behaviorSeedRef.current = emptyPersonaBehaviorDraft;
setShowAdvancedFields(false);
setIsAvatarUploadPending(false);
- // isRuntimeAutoSeededRef and hasSeededForOpenRef are NOT reset here — the
- // [initialValues, open] effect resets both when the dialog re-opens.
+ // The open-seeding effect resets both refs on the next open.
}
onOpenChange(next);
}
async function handleSubmit() {
- // D1: the same localModeSatisfied gate as canSubmit prevents form-submit
- // (Enter) from bypassing a missing credential.
+ // Keep Enter submission on the same credential gate as the button.
if (!initialValues || !localModeSatisfied || !canSubmit) return;
const {
@@ -334,11 +356,7 @@ export function AgentDefinitionDialog({
(runtime.trim().length > 0 && runtimeCanChooseLlmProvider) ||
blankRuntimeModelProviderEditable;
const trimmedProvider = provider.trim();
- // Required credential env keys for this runtime + provider combination.
- // Used to show required markers on the LLM provider label and amber
- // locked rows in the env vars editor.
- // File-layer config for the selected runtime (e.g. goose config.yaml).
- // Used to silence requirements already satisfied there.
+ // File config satisfies credentials before the readiness gate renders them.
const { data: runtimeFileConfig, isLoading: fileConfigLoading } =
useRuntimeFileConfigQuery(runtime, { enabled: open });
function handleAiConfigurationModeChange(nextMode: AgentAiConfigurationMode) {
@@ -420,25 +438,11 @@ export function AgentDefinitionDialog({
secretEnvVar: topLevelSecretEnvVar,
value: apiKeyValue,
} = apiKeyFieldState;
- // Provider required-ness is a static property of the field's visibility — it
- // does not change based on whether the field is currently filled. Using the
- // dynamic missingNormalizedFields check would flip the asterisk off once a
- // value is selected, which is incoherent (required means required, not
- // "required until satisfied"). runtimeCanChooseLlmProvider is the authoritative
- // gate: it tracks exactly when the provider picker is shown (Buzz Agent/Goose,
- // plus runtime-less legacy/builtin definitions), so the required marker never
- // drifts from whether Save actually needs a provider.
const providerIsRequired =
aiConfigurationMode === "custom" && runtimeCanChooseLlmProvider;
const modelFieldVisible =
runtime.trim().length > 0 || blankRuntimeModelProviderEditable;
const isExplicitModelRequired = aiConfigurationMode === "custom";
- // Gate the provider requirement on the field's actual visibility, not the raw
- // runtime capability. Codex/Claude hide the provider picker (they drive their
- // own provider), so Customize must not require a provider there. But a
- // runtime-less legacy/builtin definition still exposes the picker via
- // blankRuntimeModelProviderEditable, so it must keep requiring a provider —
- // otherwise Save could persist `provider: undefined` despite the visible field.
const customAiPairSatisfied = agentAiConfigurationModeSatisfied(
aiConfigurationMode,
{ provider, model },
@@ -448,8 +452,7 @@ export function AgentDefinitionDialog({
const selectedRuntimeIsAvailable =
runtime.trim().length === 0 ||
selectedRuntime?.availability === "available";
- // Gate model/provider validity through missingNormalizedFields — single
- // source of truth with the readiness gate so display and Save can't drift.
+ // Keep model/provider validity aligned with the readiness gate.
const canSubmit =
canSubmitPersonaDialog({ displayName, isPending }) &&
(!isCreateMode || runtime.trim().length > 0) &&
@@ -574,6 +577,7 @@ export function AgentDefinitionDialog({
})),
];
if (
+ !isCreateMode &&
runtime.trim().length > 0 &&
!runtimeDropdownOptions.some((option) => option.value === runtime)
) {
diff --git a/desktop/src/features/channel-templates/useApplyTemplate.ts b/desktop/src/features/channel-templates/useApplyTemplate.ts
index 1f7d066fad..474767acf4 100644
--- a/desktop/src/features/channel-templates/useApplyTemplate.ts
+++ b/desktop/src/features/channel-templates/useApplyTemplate.ts
@@ -91,8 +91,9 @@ export function useApplyTemplate() {
runtimes,
defaultProvider,
);
+ if (!resolved.runtime) continue;
inputs.push({
- runtime: resolved.runtime ?? defaultProvider,
+ runtime: resolved.runtime,
name: persona.displayName,
personaId: persona.id,
systemPrompt: persona.systemPrompt,
@@ -116,8 +117,9 @@ export function useApplyTemplate() {
runtimes,
defaultProvider,
);
+ if (!resolved.runtime) continue;
inputs.push({
- runtime: resolved.runtime ?? defaultProvider,
+ runtime: resolved.runtime,
name: persona.displayName,
personaId: persona.id,
systemPrompt: persona.systemPrompt,
diff --git a/desktop/src/features/channels/ui/AddChannelBotDialog.tsx b/desktop/src/features/channels/ui/AddChannelBotDialog.tsx
index fb27c659d5..2c5a7515cb 100644
--- a/desktop/src/features/channels/ui/AddChannelBotDialog.tsx
+++ b/desktop/src/features/channels/ui/AddChannelBotDialog.tsx
@@ -8,7 +8,10 @@ import {
type CreateChannelManagedAgentResult,
} from "@/features/agents/hooks";
import { getActivePersonas } from "@/features/agents/lib/catalog";
-import { resolvePersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime";
+import {
+ canResolveAllPersonaRuntimes,
+ resolvePersonaRuntime,
+} from "@/features/agents/lib/resolvePersonaRuntime";
import { getUsableTeams } from "@/features/agents/lib/teamPersonas";
import { AddChannelBotPersonasSection } from "@/features/channels/ui/AddChannelBotPersonasSection";
import { AddChannelBotTeamsSection } from "@/features/channels/ui/AddChannelBotTeamsSection";
@@ -135,26 +138,35 @@ export function AddChannelBotDialog({
}
async function handleSubmit() {
- if (providers.length === 0 || selectedPersonas.length === 0) return;
+ if (
+ providers.length === 0 ||
+ selectedPersonas.length === 0 ||
+ !selectedPersonasResolvable
+ ) {
+ return;
+ }
- const inputs = selectedPersonas.map((persona) => {
+ const inputs = selectedPersonas.flatMap((persona) => {
const resolved = resolvePersonaRuntime(
persona.runtime,
providers,
providers[0] ?? null,
false,
);
- return {
- runtime: resolved.runtime ?? providers[0],
- name: persona.displayName,
- personaId: persona.id,
- harnessOverride: false,
- systemPrompt: persona.systemPrompt,
- avatarUrl: persona.avatarUrl ?? undefined,
- model: persona.model ?? undefined,
- role: "bot" as const,
- backend: { type: "local" as const },
- };
+ if (!resolved.runtime) return [];
+ return [
+ {
+ runtime: resolved.runtime,
+ name: persona.displayName,
+ personaId: persona.id,
+ harnessOverride: false,
+ systemPrompt: persona.systemPrompt,
+ avatarUrl: persona.avatarUrl ?? undefined,
+ model: persona.model ?? undefined,
+ role: "bot" as const,
+ backend: { type: "local" as const },
+ },
+ ];
});
setSubmissionNotice(null);
@@ -189,9 +201,15 @@ export function AddChannelBotDialog({
}
}
+ const selectedPersonasResolvable = canResolveAllPersonaRuntimes(
+ selectedPersonas,
+ providers,
+ providers[0] ?? null,
+ );
const canSubmit =
providers.length > 0 &&
selectedPersonas.length > 0 &&
+ selectedPersonasResolvable &&
!providersLoading &&
!createBotsMutation.isPending;
const addButtonLabel = createBotsMutation.isPending
From 616e9d76258427d6163e12093b2fef8c73010ede Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Thu, 23 Jul 2026 17:12:14 -0700
Subject: [PATCH 12/22] Preserve visible harness fallbacks
---
desktop/src/features/agents/channelAgents.ts | 7 +++----
.../channel-templates/useApplyTemplate.ts | 15 +++++++++++++--
.../features/channels/ui/AddChannelBotDialog.tsx | 6 +++++-
3 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/desktop/src/features/agents/channelAgents.ts b/desktop/src/features/agents/channelAgents.ts
index edbb106817..7897bdcb6a 100644
--- a/desktop/src/features/agents/channelAgents.ts
+++ b/desktop/src/features/agents/channelAgents.ts
@@ -61,10 +61,9 @@ export type CreateChannelManagedAgentInput = {
/** Team this instance is deployed from; prevents cross-team reuse. */
teamId?: string | null;
/**
- * True when `runtime` is a runtime the user deliberately picked to override
- * the persona (a deploy-dialog runtime selector), as opposed to a
- * missing-runtime fallback. Forwarded to the backend so a persona-backed
- * create only pins the harness for a deliberate override.
+ * Pins `runtime` when the persona has no harness or the selection matches
+ * its harness. A fallback away from an unavailable configured harness stays
+ * unpinned so the persona remains authoritative.
*/
harnessOverride?: boolean;
/** Preferred model ID from the persona. Passed to createManagedAgent. */
diff --git a/desktop/src/features/channel-templates/useApplyTemplate.ts b/desktop/src/features/channel-templates/useApplyTemplate.ts
index 474767acf4..dbb91ce675 100644
--- a/desktop/src/features/channel-templates/useApplyTemplate.ts
+++ b/desktop/src/features/channel-templates/useApplyTemplate.ts
@@ -9,6 +9,7 @@ import {
usePersonasQuery,
useTeamsQuery,
} from "@/features/agents/hooks";
+import { shouldPinSelectedRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import { resolvePersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime";
import { resolveTeamPersonas } from "@/features/agents/lib/teamPersonas";
import { useLastRuntime } from "@/features/agents/lib/useLastRuntime";
@@ -86,8 +87,9 @@ export function useApplyTemplate() {
if (!persona) continue;
if (seenPersonaIds.has(persona.id)) continue;
seenPersonaIds.add(persona.id);
+ const requestedRuntimeId = entry.runtime ?? persona.runtime;
const resolved = resolvePersonaRuntime(
- entry.runtime ?? persona.runtime,
+ requestedRuntimeId,
runtimes,
defaultProvider,
);
@@ -96,6 +98,10 @@ export function useApplyTemplate() {
runtime: resolved.runtime,
name: persona.displayName,
personaId: persona.id,
+ harnessOverride: shouldPinSelectedRuntimeForDefinition(
+ requestedRuntimeId,
+ resolved.runtime.id,
+ ),
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
model: entry.model ?? persona.model ?? undefined,
@@ -112,8 +118,9 @@ export function useApplyTemplate() {
for (const persona of resolvedPersonas) {
if (seenPersonaIds.has(persona.id)) continue;
seenPersonaIds.add(persona.id);
+ const requestedRuntimeId = teamEntry.runtime ?? persona.runtime;
const resolved = resolvePersonaRuntime(
- teamEntry.runtime ?? persona.runtime,
+ requestedRuntimeId,
runtimes,
defaultProvider,
);
@@ -122,6 +129,10 @@ export function useApplyTemplate() {
runtime: resolved.runtime,
name: persona.displayName,
personaId: persona.id,
+ harnessOverride: shouldPinSelectedRuntimeForDefinition(
+ requestedRuntimeId,
+ resolved.runtime.id,
+ ),
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
model: teamEntry.model ?? persona.model ?? undefined,
diff --git a/desktop/src/features/channels/ui/AddChannelBotDialog.tsx b/desktop/src/features/channels/ui/AddChannelBotDialog.tsx
index 2c5a7515cb..63bf6f4382 100644
--- a/desktop/src/features/channels/ui/AddChannelBotDialog.tsx
+++ b/desktop/src/features/channels/ui/AddChannelBotDialog.tsx
@@ -12,6 +12,7 @@ import {
canResolveAllPersonaRuntimes,
resolvePersonaRuntime,
} from "@/features/agents/lib/resolvePersonaRuntime";
+import { shouldPinSelectedRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import { getUsableTeams } from "@/features/agents/lib/teamPersonas";
import { AddChannelBotPersonasSection } from "@/features/channels/ui/AddChannelBotPersonasSection";
import { AddChannelBotTeamsSection } from "@/features/channels/ui/AddChannelBotTeamsSection";
@@ -159,7 +160,10 @@ export function AddChannelBotDialog({
runtime: resolved.runtime,
name: persona.displayName,
personaId: persona.id,
- harnessOverride: false,
+ harnessOverride: shouldPinSelectedRuntimeForDefinition(
+ persona.runtime,
+ resolved.runtime.id,
+ ),
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
model: persona.model ?? undefined,
From bb5b0330f405818d4dace9bcaec77bd8d9bc3edf Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Thu, 23 Jul 2026 17:26:40 -0700
Subject: [PATCH 13/22] Complete hidden harness provisioning policy
---
.../src/managed_agents/discovery/overrides.rs | 10 +++----
.../src/managed_agents/discovery/tests.rs | 16 +---------
.../discovery/tests/provisioning_overrides.rs | 19 ++++++++++++
.../src/managed_agents/global_config/mod.rs | 2 +-
.../src/managed_agents/types/requests.rs | 7 ++---
desktop/src/features/agents/AGENTS.md | 12 +++++---
.../lib/instanceInputForDefinition.test.mjs | 28 ++++++++++++++++++
.../agents/lib/instanceInputForDefinition.ts | 29 +++++++++++++++++++
.../features/agents/ui/AgentConfigFields.tsx | 27 +++++++----------
.../channel-templates/useApplyTemplate.ts | 26 +++++------------
.../channels/ui/AddChannelBotDialog.tsx | 16 +++-------
.../features/channels/ui/useQuickBotDrop.ts | 13 ++++-----
.../messages/ui/useMentionSendFlow.ts | 11 +++----
13 files changed, 125 insertions(+), 91 deletions(-)
create mode 100644 desktop/src-tauri/src/managed_agents/discovery/tests/provisioning_overrides.rs
diff --git a/desktop/src-tauri/src/managed_agents/discovery/overrides.rs b/desktop/src-tauri/src/managed_agents/discovery/overrides.rs
index 5140bb2cdd..7f9ded5a18 100644
--- a/desktop/src-tauri/src/managed_agents/discovery/overrides.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery/overrides.rs
@@ -113,10 +113,10 @@ pub fn apply_agent_command_update(
/// `resolvePersonaRuntime` (frontend), which produces a divergent command in two
/// distinct cases that the backend MUST tell apart:
///
-/// - DELIBERATE OVERRIDE (`harness_override` true): the user explicitly picked a
-/// runtime command in UI that exposes a runtime selector. This is a real pin
-/// and is preserved when it differs from the command inheritance would spawn,
-/// including installed aliases such as `claude-code-acp`.
+/// - PINNED SELECTION (`harness_override` true): the frontend selected a runtime
+/// that must survive persona inheritance. This includes explicit user choices,
+/// installed aliases such as `claude-code-acp`, and visible implicit fallbacks
+/// for runtime-less personas.
/// - MISSING-RUNTIME FALLBACK (`harness_override` false): the persona's runtime
/// isn't installed locally, so `resolvePersonaRuntime` substitutes a fallback
/// default. This is NOT a pin — baking it would freeze the agent on the fallback
@@ -125,7 +125,7 @@ pub fn apply_agent_command_update(
/// so the persona stays authoritative.
///
/// `isOverridden` from `resolvePersonaRuntime` cannot distinguish these — it is
-/// `true` for BOTH — so the caller must thread the explicit user-intent bit.
+/// `true` for BOTH — so the caller must thread the pinning decision.
///
/// Persona-less creates (`persona_id` is `None`, e.g. the standalone
/// CreateAgentDialog) have no persona to inherit, so the picked command is always a
diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs
index 0ed4fe0f6a..7e758456a0 100644
--- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs
@@ -419,21 +419,6 @@ fn create_time_override_none_when_persona_runtime_not_installed() {
);
}
-#[test]
-fn create_time_override_some_when_user_deliberately_overrides_installed_runtime() {
- // Case 2 + deliberate override: the persona's `claude` runtime IS
- // available, but the user explicitly picked `codex` in a deploy dialog's
- // runtime selector ("overriding persona preferences"), so the frontend
- // sends `codex-acp` with `harness_override` true. This is a real pin and
- // MUST be preserved — returning `None` would silently swallow the
- // deliberate override and inherit `claude` on spawn.
- let personas = vec![persona_with_runtime("p1", Some("claude"))];
- assert_eq!(
- create_time_agent_command_override(Some("p1"), &personas, Some("codex-acp"), true),
- Some("codex-acp".to_string())
- );
-}
-
#[test]
fn create_time_override_none_when_persona_runtime_installed() {
// Case 2: the persona's runtime is available, so `resolvePersonaRuntime`
@@ -610,6 +595,7 @@ fn apply_agent_command_update_concrete_pin_keeps_materialized_runtime() {
// ── probe_codex_acp_major_version ─────────────────────────────────────────────
mod managed_path_resolution;
+mod provisioning_overrides;
#[cfg(unix)]
#[test]
diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests/provisioning_overrides.rs b/desktop/src-tauri/src/managed_agents/discovery/tests/provisioning_overrides.rs
new file mode 100644
index 0000000000..972d05fdf2
--- /dev/null
+++ b/desktop/src-tauri/src/managed_agents/discovery/tests/provisioning_overrides.rs
@@ -0,0 +1,19 @@
+use super::{create_time_agent_command_override, persona_with_runtime};
+
+#[test]
+fn preserves_deliberate_override_of_installed_runtime() {
+ let personas = vec![persona_with_runtime("p1", Some("claude"))];
+ assert_eq!(
+ create_time_agent_command_override(Some("p1"), &personas, Some("codex-acp"), true),
+ Some("codex-acp".to_string())
+ );
+}
+
+#[test]
+fn pins_visible_fallback_for_runtime_less_persona() {
+ let personas = vec![persona_with_runtime("p1", None)];
+ assert_eq!(
+ create_time_agent_command_override(Some("p1"), &personas, Some("goose"), true),
+ Some("goose".to_string())
+ );
+}
diff --git a/desktop/src-tauri/src/managed_agents/global_config/mod.rs b/desktop/src-tauri/src/managed_agents/global_config/mod.rs
index 69bc39b58e..7ff2773474 100644
--- a/desktop/src-tauri/src/managed_agents/global_config/mod.rs
+++ b/desktop/src-tauri/src/managed_agents/global_config/mod.rs
@@ -242,7 +242,7 @@ pub(crate) fn global_model_provider_for_record<'a>(
let selected_command = crate::managed_agents::record_agent_command(record, personas);
let selected_runtime = crate::managed_agents::known_acp_runtime(&selected_command);
- if selected_runtime.is_some_and(|selected| std::ptr::eq(preferred_runtime, selected)) {
+ if selected_runtime.is_some_and(|selected| preferred_runtime.id == selected.id) {
global_values
} else {
(None, None)
diff --git a/desktop/src-tauri/src/managed_agents/types/requests.rs b/desktop/src-tauri/src/managed_agents/types/requests.rs
index 58d60218a1..6d882de266 100644
--- a/desktop/src-tauri/src/managed_agents/types/requests.rs
+++ b/desktop/src-tauri/src/managed_agents/types/requests.rs
@@ -135,10 +135,9 @@ pub struct CreateManagedAgentRequest {
pub relay_url: Option,
pub acp_command: Option,
pub agent_command: Option,
- /// True when `agent_command` is a runtime command the user deliberately
- /// picked for a linked persona. Distinguishes a real selection, including an
- /// installed alias, from a missing-runtime fallback so a persona-backed
- /// create only stores an `agent_command_override` for the former.
+ /// True when `agent_command` must survive linked-persona inheritance.
+ /// Includes explicit selections, installed aliases, and the visible
+ /// implicit fallback for a runtime-less persona.
#[serde(default)]
pub harness_override: bool,
#[serde(default)]
diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md
index 544981cb91..4ba4827efb 100644
--- a/desktop/src/features/agents/AGENTS.md
+++ b/desktop/src/features/agents/AGENTS.md
@@ -96,10 +96,14 @@ with a TypeScript lookup table or an id comparison in a component.
runtime-less deployment or provisioning path. Definition-to-instance starts
use the stricter `resolveStartRuntimeForDefinition` wrapper; call one of
these shared resolvers instead of duplicating default selection in a start
- surface. Team deploys use the same visible-runtime set only for members that
- need an implicit fallback, so a fully pinned team remains deployable even
- when every installed runtime is hidden. Definitions already pinned to a
- hidden runtime remain runnable.
+ surface. Persona-backed provisioning uses
+ `resolveProvisioningRuntimeForDefinition` so the resolved runtime and
+ backend pinning bit cannot drift apart. Team deploys use the same
+ visible-runtime set only for members that need an implicit fallback, so a
+ fully pinned team remains deployable even when every installed runtime is
+ hidden. Definitions already pinned to a hidden runtime remain runnable.
+ Provider pickers use `getPersonaHiddenProviderIds` as the shared
+ relay-mesh visibility policy.
## The tests that enforce this
diff --git a/desktop/src/features/agents/lib/instanceInputForDefinition.test.mjs b/desktop/src/features/agents/lib/instanceInputForDefinition.test.mjs
index 90d85f526b..58c9d9a312 100644
--- a/desktop/src/features/agents/lib/instanceInputForDefinition.test.mjs
+++ b/desktop/src/features/agents/lib/instanceInputForDefinition.test.mjs
@@ -4,6 +4,7 @@ import test from "node:test";
import {
availableRuntimesForStart,
buildInstanceInputForDefinition,
+ resolveProvisioningRuntimeForDefinition,
resolveStartRuntimeForDefinition,
shouldPinSelectedRuntimeForDefinition,
} from "./instanceInputForDefinition.ts";
@@ -101,6 +102,33 @@ test("row 2: harnessOverride follows the backend-aligned formula", async () => {
);
});
+test("provisioning pins a visible fallback for a runtime-less definition", () => {
+ const resolved = resolveProvisioningRuntimeForDefinition(
+ null,
+ [buzzAgentRuntime, gooseRuntime],
+ null,
+ ["buzz-agent"],
+ );
+ assert.equal(resolved.runtime, gooseRuntime);
+ assert.equal(resolved.harnessOverride, true);
+});
+
+test("provisioning uses product ordering and leaves unavailable configured fallbacks unpinned", () => {
+ const ordered = resolveProvisioningRuntimeForDefinition(null, [
+ gooseRuntime,
+ claudeRuntime,
+ buzzAgentRuntime,
+ ]);
+ assert.equal(ordered.runtime, buzzAgentRuntime);
+ assert.equal(ordered.harnessOverride, true);
+
+ const unavailable = resolveProvisioningRuntimeForDefinition("missing", [
+ gooseRuntime,
+ ]);
+ assert.equal(unavailable.runtime, gooseRuntime);
+ assert.equal(unavailable.harnessOverride, false);
+});
+
test("row 3: plain avatar URLs pass through; base64 data URIs upload via the injectable", async () => {
const plain = await buildInstanceInputForDefinition(persona(), gooseRuntime);
assert.equal(plain.avatarUrl, "https://example.com/a.png");
diff --git a/desktop/src/features/agents/lib/instanceInputForDefinition.ts b/desktop/src/features/agents/lib/instanceInputForDefinition.ts
index dda66ae69a..0da53bf2ef 100644
--- a/desktop/src/features/agents/lib/instanceInputForDefinition.ts
+++ b/desktop/src/features/agents/lib/instanceInputForDefinition.ts
@@ -111,6 +111,35 @@ export function shouldPinSelectedRuntimeForDefinition(
);
}
+/**
+ * Resolve the runtime and backend pinning bit together so persona-backed
+ * provisioning cannot discard a visible implicit fallback during handoff.
+ */
+export function resolveProvisioningRuntimeForDefinition(
+ definitionRuntimeId: string | null | undefined,
+ runtimes: readonly AcpRuntime[],
+ preferredRuntimeId?: string | null,
+ disabledRuntimeIds: readonly string[] = getDisabledAcpRuntimeIdsSnapshot(),
+): ResolvePersonaRuntimeResult & { harnessOverride: boolean } {
+ const defaultRuntime = getDefaultPersonaRuntime(runtimes, preferredRuntimeId);
+ const resolved = resolvePersonaRuntime(
+ definitionRuntimeId,
+ runtimes,
+ defaultRuntime,
+ false,
+ disabledRuntimeIds,
+ );
+ return {
+ ...resolved,
+ harnessOverride:
+ resolved.runtime !== null &&
+ shouldPinSelectedRuntimeForDefinition(
+ definitionRuntimeId,
+ resolved.runtime.id,
+ ),
+ };
+}
+
/**
* The single definition→instance mapping (Phase 1B.3.5 rows 2–4). Every
* surface that creates a running instance from a definition builds its
diff --git a/desktop/src/features/agents/ui/AgentConfigFields.tsx b/desktop/src/features/agents/ui/AgentConfigFields.tsx
index 4e5ba17607..77eb9560de 100644
--- a/desktop/src/features/agents/ui/AgentConfigFields.tsx
+++ b/desktop/src/features/agents/ui/AgentConfigFields.tsx
@@ -27,8 +27,8 @@ import {
} from "@/features/agents/ui/bakedEnvHelpers";
import {
AUTO_PROVIDER_DROPDOWN_VALUE,
- BLOCK_BUILD_HIDDEN_PROVIDER_IDS,
CUSTOM_PROVIDER_DROPDOWN_VALUE,
+ getPersonaHiddenProviderIds,
getPersonaProviderOptions,
getProviderApiKeyEnvVar,
requiredCredentialEnvKeys,
@@ -543,21 +543,16 @@ export function AgentConfigFields({
onConfigChange({ ...config, env_vars: merged });
}
- // On internal Block builds, BUZZ_AGENT_PROVIDER is baked in and a boot
- // migration rewrites v1→v2. Hide the legacy v1 option so it is not offered
- // for new selections; OSS builds show it.
- const hideProviderIds = React.useMemo(() => {
- const hidden = new Set();
- if (bakedEnvKeys.includes("BUZZ_AGENT_PROVIDER")) {
- for (const providerId of BLOCK_BUILD_HIDDEN_PROVIDER_IDS) {
- hidden.add(providerId);
- }
- }
- if (selectedRuntimeId !== "buzz-agent") {
- hidden.add("relay-mesh");
- }
- return hidden;
- }, [bakedEnvKeys, selectedRuntimeId]);
+ const hideProviderIds = React.useMemo(
+ () =>
+ getPersonaHiddenProviderIds({
+ bakedEnvKeys,
+ selectableRuntimes: selectedRuntime ? [selectedRuntime] : [],
+ currentRuntimeId: selectedRuntimeId,
+ preserveCurrentRuntime: false,
+ }),
+ [bakedEnvKeys, selectedRuntime, selectedRuntimeId],
+ );
const providerOptions = getPersonaProviderOptions(
providerValue,
credentialRuntimeId,
diff --git a/desktop/src/features/channel-templates/useApplyTemplate.ts b/desktop/src/features/channel-templates/useApplyTemplate.ts
index dbb91ce675..537ca66f7b 100644
--- a/desktop/src/features/channel-templates/useApplyTemplate.ts
+++ b/desktop/src/features/channel-templates/useApplyTemplate.ts
@@ -9,8 +9,7 @@ import {
usePersonasQuery,
useTeamsQuery,
} from "@/features/agents/hooks";
-import { shouldPinSelectedRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
-import { resolvePersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime";
+import { resolveProvisioningRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import { resolveTeamPersonas } from "@/features/agents/lib/teamPersonas";
import { useLastRuntime } from "@/features/agents/lib/useLastRuntime";
import { useChannelTemplatesQuery } from "@/features/channel-templates/hooks";
@@ -73,11 +72,6 @@ export function useApplyTemplate() {
const runtimes = acpRuntimesQuery.data ?? [];
if (runtimes.length === 0) return; // No runtimes — skip silently
- // Resolve default provider: user's last-used preference, or first available
- const defaultProvider =
- runtimes.find((p) => p.id === lastRuntimeId) ?? runtimes[0] ?? null;
- if (!defaultProvider) return;
-
const seenPersonaIds = new Set();
const inputs: CreateChannelManagedAgentInput[] = [];
@@ -88,20 +82,17 @@ export function useApplyTemplate() {
if (seenPersonaIds.has(persona.id)) continue;
seenPersonaIds.add(persona.id);
const requestedRuntimeId = entry.runtime ?? persona.runtime;
- const resolved = resolvePersonaRuntime(
+ const resolved = resolveProvisioningRuntimeForDefinition(
requestedRuntimeId,
runtimes,
- defaultProvider,
+ lastRuntimeId,
);
if (!resolved.runtime) continue;
inputs.push({
runtime: resolved.runtime,
name: persona.displayName,
personaId: persona.id,
- harnessOverride: shouldPinSelectedRuntimeForDefinition(
- requestedRuntimeId,
- resolved.runtime.id,
- ),
+ harnessOverride: resolved.harnessOverride,
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
model: entry.model ?? persona.model ?? undefined,
@@ -119,20 +110,17 @@ export function useApplyTemplate() {
if (seenPersonaIds.has(persona.id)) continue;
seenPersonaIds.add(persona.id);
const requestedRuntimeId = teamEntry.runtime ?? persona.runtime;
- const resolved = resolvePersonaRuntime(
+ const resolved = resolveProvisioningRuntimeForDefinition(
requestedRuntimeId,
runtimes,
- defaultProvider,
+ lastRuntimeId,
);
if (!resolved.runtime) continue;
inputs.push({
runtime: resolved.runtime,
name: persona.displayName,
personaId: persona.id,
- harnessOverride: shouldPinSelectedRuntimeForDefinition(
- requestedRuntimeId,
- resolved.runtime.id,
- ),
+ harnessOverride: resolved.harnessOverride,
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
model: teamEntry.model ?? persona.model ?? undefined,
diff --git a/desktop/src/features/channels/ui/AddChannelBotDialog.tsx b/desktop/src/features/channels/ui/AddChannelBotDialog.tsx
index 63bf6f4382..cd8e8beb80 100644
--- a/desktop/src/features/channels/ui/AddChannelBotDialog.tsx
+++ b/desktop/src/features/channels/ui/AddChannelBotDialog.tsx
@@ -8,11 +8,8 @@ import {
type CreateChannelManagedAgentResult,
} from "@/features/agents/hooks";
import { getActivePersonas } from "@/features/agents/lib/catalog";
-import {
- canResolveAllPersonaRuntimes,
- resolvePersonaRuntime,
-} from "@/features/agents/lib/resolvePersonaRuntime";
-import { shouldPinSelectedRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
+import { resolveProvisioningRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
+import { canResolveAllPersonaRuntimes } from "@/features/agents/lib/resolvePersonaRuntime";
import { getUsableTeams } from "@/features/agents/lib/teamPersonas";
import { AddChannelBotPersonasSection } from "@/features/channels/ui/AddChannelBotPersonasSection";
import { AddChannelBotTeamsSection } from "@/features/channels/ui/AddChannelBotTeamsSection";
@@ -148,11 +145,9 @@ export function AddChannelBotDialog({
}
const inputs = selectedPersonas.flatMap((persona) => {
- const resolved = resolvePersonaRuntime(
+ const resolved = resolveProvisioningRuntimeForDefinition(
persona.runtime,
providers,
- providers[0] ?? null,
- false,
);
if (!resolved.runtime) return [];
return [
@@ -160,10 +155,7 @@ export function AddChannelBotDialog({
runtime: resolved.runtime,
name: persona.displayName,
personaId: persona.id,
- harnessOverride: shouldPinSelectedRuntimeForDefinition(
- persona.runtime,
- resolved.runtime.id,
- ),
+ harnessOverride: resolved.harnessOverride,
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
model: persona.model ?? undefined,
diff --git a/desktop/src/features/channels/ui/useQuickBotDrop.ts b/desktop/src/features/channels/ui/useQuickBotDrop.ts
index f141ca4381..5283a402fb 100644
--- a/desktop/src/features/channels/ui/useQuickBotDrop.ts
+++ b/desktop/src/features/channels/ui/useQuickBotDrop.ts
@@ -4,7 +4,7 @@ import {
useAvailableAcpRuntimes,
useCreateChannelManagedAgentMutation,
} from "@/features/agents/hooks";
-import { resolvePersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime";
+import { resolveProvisioningRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import type { AgentPersona } from "@/shared/api/types";
type QuickBotDropState = {
@@ -24,7 +24,6 @@ export function useQuickBotDrop(channelId: string | null) {
});
const providers = providersQuery.data ?? [];
- const defaultProvider = providers[0] ?? null;
const addBot = React.useCallback(
async (persona: AgentPersona, instanceName: string) => {
@@ -33,11 +32,8 @@ export function useQuickBotDrop(channelId: string | null) {
setState({ pending: true, error: null });
try {
- const { runtime } = resolvePersonaRuntime(
- persona.runtime,
- providers,
- defaultProvider,
- );
+ const { harnessOverride, runtime } =
+ resolveProvisioningRuntimeForDefinition(persona.runtime, providers);
if (!runtime) {
setState({
@@ -54,6 +50,7 @@ export function useQuickBotDrop(channelId: string | null) {
avatarUrl: persona.avatarUrl ?? undefined,
personaId: persona.id,
model: persona.model ?? undefined,
+ harnessOverride,
});
setState({ pending: false, error: null });
@@ -64,7 +61,7 @@ export function useQuickBotDrop(channelId: string | null) {
});
}
},
- [channelId, createMutation, defaultProvider, providers, state.pending],
+ [channelId, createMutation, providers, state.pending],
);
return { ...state, addBot };
diff --git a/desktop/src/features/messages/ui/useMentionSendFlow.ts b/desktop/src/features/messages/ui/useMentionSendFlow.ts
index 6d4e007cd4..1a95ae8755 100644
--- a/desktop/src/features/messages/ui/useMentionSendFlow.ts
+++ b/desktop/src/features/messages/ui/useMentionSendFlow.ts
@@ -10,7 +10,7 @@ import {
useProvisionChannelManagedAgentMutation,
useStartManagedAgentMutation,
} from "@/features/agents/hooks";
-import { resolvePersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime";
+import { resolveProvisioningRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import { useAddChannelMembersMutation } from "@/features/channels/hooks";
import { filterEffectiveExplicitAgentPubkeys } from "@/features/messages/lib/effectiveExplicitAgentPubkeys";
import type { UseChannelLinksResult } from "@/features/messages/lib/useChannelLinks";
@@ -313,7 +313,6 @@ export function useMentionSendFlow({
}
const runtimes = await getAvailableRuntimes();
- const defaultRuntime = runtimes[0] ?? null;
const errors: string[] = [];
const agents: ManagedAgent[] = [];
const pubkeys: string[] = [];
@@ -327,11 +326,8 @@ export function useMentionSendFlow({
}
seenPersonaIds.add(persona.id);
- const { runtime } = resolvePersonaRuntime(
- persona.runtime,
- runtimes,
- defaultRuntime,
- );
+ const { harnessOverride, runtime } =
+ resolveProvisioningRuntimeForDefinition(persona.runtime, runtimes);
if (!runtime) {
errors.push(`${displayName}: No agent runtime available.`);
continue;
@@ -345,6 +341,7 @@ export function useMentionSendFlow({
runtime,
name: persona.displayName,
personaId: persona.id,
+ harnessOverride,
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
model: persona.model ?? undefined,
From 4ba301d87d42bdebdc10675a1507671287b8fab4 Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Thu, 23 Jul 2026 18:00:34 -0700
Subject: [PATCH 14/22] Address harness visibility review feedback
---
desktop/src/features/agents/AGENTS.md | 6 ++-
.../features/agents/ui/AgentConfigFields.tsx | 4 +-
.../agents/ui/AgentDefinitionDialog.tsx | 14 +++++--
.../agents/ui/AgentInstanceEditDialog.tsx | 18 ++++----
.../agents/ui/agentConfigOptions.test.mjs | 41 +++++++++++++++++--
.../features/agents/ui/agentConfigOptions.tsx | 35 ++++++++++++----
.../features/onboarding/welcomeGuide.test.mjs | 32 +++++++++++++++
.../src/features/onboarding/welcomeGuide.ts | 30 +++++++++++++-
8 files changed, 154 insertions(+), 26 deletions(-)
diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md
index 431e295cc2..867e5d8eb2 100644
--- a/desktop/src/features/agents/AGENTS.md
+++ b/desktop/src/features/agents/AGENTS.md
@@ -102,8 +102,10 @@ with a TypeScript lookup table or an id comparison in a component.
visible-runtime set only for members that need an implicit fallback, so a
fully pinned team remains deployable even when every installed runtime is
hidden. Definitions already pinned to a hidden runtime remain runnable.
- Provider pickers use `getPersonaHiddenProviderIds` as the shared
- relay-mesh visibility policy.
+ Welcome Team reconciliation skips runtime updates for existing agents on a
+ hidden runtime. Provider pickers use `getPersonaHiddenProviderIds` as the
+ shared relay-mesh visibility policy, deriving support from each runtime's
+ catalog-provided `providerEnvVar` rather than its ID.
10. **The defaults modal is progressively disclosed.** An unset global config
starts on the Buzz Agent-first deployment fallback and carries that visible
harness into the next saved edit. The `progressive-defaults` disclosure
diff --git a/desktop/src/features/agents/ui/AgentConfigFields.tsx b/desktop/src/features/agents/ui/AgentConfigFields.tsx
index 5afe782637..834c034f31 100644
--- a/desktop/src/features/agents/ui/AgentConfigFields.tsx
+++ b/desktop/src/features/agents/ui/AgentConfigFields.tsx
@@ -590,10 +590,10 @@ export function AgentConfigFields({
getPersonaHiddenProviderIds({
bakedEnvKeys,
selectableRuntimes: selectedRuntime ? [selectedRuntime] : [],
- currentRuntimeId: selectedRuntimeId,
+ currentRuntime: selectedRuntime,
preserveCurrentRuntime: false,
}),
- [bakedEnvKeys, selectedRuntime, selectedRuntimeId],
+ [bakedEnvKeys, selectedRuntime],
);
const providerOptions = getPersonaProviderOptions(
providerValue,
diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
index 74d9a60c82..f13ed8b73b 100644
--- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
@@ -36,6 +36,7 @@ import {
getPersonaHiddenProviderIds,
getPersonaModelOptions,
getPersonaProviderOptions,
+ getRelayMeshRuntime,
getRuntimePersonaModelOptions,
NO_RUNTIME_DROPDOWN_VALUE,
runtimeSupportsLlmProviderSelection,
@@ -516,7 +517,7 @@ export function AgentDefinitionDialog({
const hideProviderIds = getPersonaHiddenProviderIds({
bakedEnvKeys: bakedEnvKeys ?? [],
selectableRuntimes,
- currentRuntimeId: runtime,
+ currentRuntime: selectedRuntime,
preserveCurrentRuntime: !isCreateMode,
});
const providerOptions = getPersonaProviderOptions(
@@ -681,11 +682,16 @@ export function AgentDefinitionDialog({
function handleProviderDropdownChange(nextValue: string) {
const nextProvider =
nextValue === AUTO_PROVIDER_DROPDOWN_VALUE ? "" : nextValue;
- if (nextProvider === "relay-mesh" && runtime !== "buzz-agent") {
- handleRuntimeDropdownChange("buzz-agent");
+ const relayMeshRuntime =
+ nextProvider === "relay-mesh"
+ ? getRelayMeshRuntime(selectableRuntimes, selectedRuntime)
+ : null;
+ const nextRuntime = relayMeshRuntime?.id ?? runtime;
+ if (nextRuntime !== runtime) {
+ handleRuntimeDropdownChange(nextRuntime);
}
const nextSelection = selectionOnProviderDropdownChange(selection, {
- runtime: nextProvider === "relay-mesh" ? "buzz-agent" : runtime,
+ runtime: nextRuntime,
nextValue,
clearModelWhenApiKeyMissing: true,
});
diff --git a/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx b/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
index d3ba615224..be13b557ef 100644
--- a/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
@@ -32,6 +32,7 @@ import {
getDefaultPersonaRuntime,
getPersonaHiddenProviderIds,
getPersonaProviderOptions,
+ getRelayMeshRuntime,
getProviderApiKeyEnvVar,
isMissingRequiredDropdownField,
NO_RUNTIME_DROPDOWN_VALUE,
@@ -527,14 +528,17 @@ export function AgentInstanceEditDialog({
function handleProviderDropdownChange(nextValue: string) {
const nextProvider =
nextValue === AUTO_PROVIDER_DROPDOWN_VALUE ? "" : nextValue;
- if (nextProvider === "relay-mesh" && selectedRuntimeId !== "buzz-agent") {
- handleRuntimeDropdownChange("buzz-agent");
+ const relayMeshRuntime =
+ nextProvider === "relay-mesh"
+ ? getRelayMeshRuntime(selectableRuntimes, selectedRuntime)
+ : null;
+ const nextRuntimeId =
+ relayMeshRuntime?.id ?? selectedRuntime?.id ?? selectedRuntimeId;
+ if (nextRuntimeId !== selectedRuntimeId) {
+ handleRuntimeDropdownChange(nextRuntimeId);
}
const nextSelection = selectionOnProviderDropdownChange(selection, {
- runtime:
- nextProvider === "relay-mesh"
- ? "buzz-agent"
- : (selectedRuntime?.id ?? selectedRuntimeId),
+ runtime: nextRuntimeId,
nextValue,
clearModelWhenApiKeyMissing: false,
});
@@ -772,7 +776,7 @@ export function AgentInstanceEditDialog({
const hideProviderIds = getPersonaHiddenProviderIds({
bakedEnvKeys: bakedEnvKeys ?? [],
selectableRuntimes,
- currentRuntimeId: selectedRuntimeId,
+ currentRuntime: selectedRuntime,
preserveCurrentRuntime: true,
});
const providerOptions = getPersonaProviderOptions(
diff --git a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
index ae078f2fc2..d2dc068f90 100644
--- a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
+++ b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
@@ -14,13 +14,22 @@ import { formatModelDiscoveryErrorStatus } from "./personaModelDiscoveryStatus.t
// ── helpers ──────────────────────────────────────────────────────────────────
-function makeRuntime(id, availability = "available") {
+function makeRuntime(
+ id,
+ availability = "available",
+ providerEnvVar = id === "buzz-agent"
+ ? "BUZZ_AGENT_PROVIDER"
+ : id === "goose"
+ ? "GOOSE_PROVIDER"
+ : null,
+) {
return {
id,
label: id,
command: id,
defaultArgs: [],
mcpCommand: null,
+ providerEnvVar,
availability,
};
}
@@ -80,7 +89,7 @@ test("hidden Buzz Agent suppresses shared compute for new selections", () => {
const hidden = getPersonaHiddenProviderIds({
bakedEnvKeys: [],
selectableRuntimes: [makeRuntime("goose")],
- currentRuntimeId: "goose",
+ currentRuntime: makeRuntime("goose"),
preserveCurrentRuntime: false,
});
const ids = getPersonaProviderOptions("", "goose", "", hidden).map(
@@ -93,7 +102,7 @@ test("an existing hidden Buzz Agent keeps its shared compute provider", () => {
const hidden = getPersonaHiddenProviderIds({
bakedEnvKeys: [],
selectableRuntimes: [makeRuntime("goose")],
- currentRuntimeId: "buzz-agent",
+ currentRuntime: makeRuntime("buzz-agent"),
preserveCurrentRuntime: true,
});
const ids = getPersonaProviderOptions("", "buzz-agent", "", hidden).map(
@@ -102,6 +111,32 @@ test("an existing hidden Buzz Agent keeps its shared compute provider", () => {
assert.ok(ids.includes("relay-mesh"));
});
+test("shared compute visibility follows runtime catalog metadata instead of runtime ids", () => {
+ const hidden = getPersonaHiddenProviderIds({
+ bakedEnvKeys: [],
+ selectableRuntimes: [
+ makeRuntime("future-shared-compute", "available", "BUZZ_AGENT_PROVIDER"),
+ ],
+ preserveCurrentRuntime: false,
+ });
+ const ids = getPersonaProviderOptions(
+ "",
+ "future-shared-compute",
+ "",
+ hidden,
+ ).map((option) => option.id);
+ assert.ok(ids.includes("relay-mesh"));
+
+ const renamedCapability = getPersonaHiddenProviderIds({
+ bakedEnvKeys: [],
+ selectableRuntimes: [
+ makeRuntime("buzz-agent", "available", "GOOSE_PROVIDER"),
+ ],
+ preserveCurrentRuntime: false,
+ });
+ assert.ok(renamedCapability.has("relay-mesh"));
+});
+
// ── getDefaultPersonaRuntime — buzz-agent first ───────────────────────────────
test("getDefaultPersonaRuntime honors an available global preference", () => {
diff --git a/desktop/src/features/agents/ui/agentConfigOptions.tsx b/desktop/src/features/agents/ui/agentConfigOptions.tsx
index fda3825e11..d8ceb01449 100644
--- a/desktop/src/features/agents/ui/agentConfigOptions.tsx
+++ b/desktop/src/features/agents/ui/agentConfigOptions.tsx
@@ -21,24 +21,45 @@ export const BLOCK_BUILD_HIDDEN_PROVIDER_IDS: ReadonlySet = new Set([
"databricks",
]);
+type RuntimeProviderCapability = Pick<
+ AcpRuntimeCatalogEntry,
+ "id" | "providerEnvVar"
+>;
+
+export function runtimeSupportsRelayMesh(
+ runtime: RuntimeProviderCapability | null | undefined,
+): boolean {
+ return runtime?.providerEnvVar === "BUZZ_AGENT_PROVIDER";
+}
+
+export function getRelayMeshRuntime(
+ selectableRuntimes: readonly T[],
+ currentRuntime?: T | null,
+): T | null {
+ if (runtimeSupportsRelayMesh(currentRuntime)) {
+ return currentRuntime ?? null;
+ }
+ return selectableRuntimes.find(runtimeSupportsRelayMesh) ?? null;
+}
+
export function getPersonaHiddenProviderIds({
bakedEnvKeys,
selectableRuntimes,
- currentRuntimeId,
+ currentRuntime,
preserveCurrentRuntime,
}: {
bakedEnvKeys: readonly string[];
- selectableRuntimes: readonly Pick[];
- currentRuntimeId: string;
+ selectableRuntimes: readonly RuntimeProviderCapability[];
+ currentRuntime?: RuntimeProviderCapability | null;
preserveCurrentRuntime: boolean;
}): ReadonlySet {
const hidden = bakedEnvKeys.includes("BUZZ_AGENT_PROVIDER")
? new Set(BLOCK_BUILD_HIDDEN_PROVIDER_IDS)
: new Set();
- const buzzAgentSelectable =
- selectableRuntimes.some((runtime) => runtime.id === "buzz-agent") ||
- (preserveCurrentRuntime && currentRuntimeId.trim() === "buzz-agent");
- if (!buzzAgentSelectable) hidden.add("relay-mesh");
+ const relayMeshSelectable =
+ selectableRuntimes.some(runtimeSupportsRelayMesh) ||
+ (preserveCurrentRuntime && runtimeSupportsRelayMesh(currentRuntime));
+ if (!relayMeshSelectable) hidden.add("relay-mesh");
return hidden;
}
diff --git a/desktop/src/features/onboarding/welcomeGuide.test.mjs b/desktop/src/features/onboarding/welcomeGuide.test.mjs
index b3def930f1..2fe0bc1b35 100644
--- a/desktop/src/features/onboarding/welcomeGuide.test.mjs
+++ b/desktop/src/features/onboarding/welcomeGuide.test.mjs
@@ -289,6 +289,38 @@ test("existing Welcome starter needs no update when runtime already matches", ()
);
});
+test("existing Welcome starter keeps its runtime when that harness is hidden", () => {
+ const existing = makeAgent({
+ personaId: WELCOME_GUIDE_PERSONA_ID,
+ agentCommand: "buzz-agent",
+ agentArgs: [],
+ model: "auto",
+ provider: "relay-mesh",
+ });
+
+ assert.equal(
+ welcomeStarterRuntimeUpdate(
+ existing,
+ {
+ name: "Fizz",
+ agentCommand: "goose",
+ agentArgs: [],
+ mcpCommand: "",
+ model: "gpt-5",
+ provider: "openai",
+ },
+ {
+ runtimes: [
+ { id: "buzz-agent", command: "buzz-agent" },
+ { id: "goose", command: "goose" },
+ ],
+ disabledRuntimeIds: ["buzz-agent"],
+ },
+ ),
+ null,
+ );
+});
+
test("welcome team starter definitions and role identities are stable", () => {
assert.equal(WELCOME_TEAM_ID, "builtin-team:welcome");
assert.deepEqual(WELCOME_TEAM_STARTERS, [
diff --git a/desktop/src/features/onboarding/welcomeGuide.ts b/desktop/src/features/onboarding/welcomeGuide.ts
index aa8deb7c19..f1f9f274de 100644
--- a/desktop/src/features/onboarding/welcomeGuide.ts
+++ b/desktop/src/features/onboarding/welcomeGuide.ts
@@ -2,6 +2,10 @@ import {
buildInstanceInputForDefinition,
resolveStartRuntimeForDefinition,
} from "@/features/agents/lib/instanceInputForDefinition";
+import {
+ filterEnabledAcpRuntimes,
+ getDisabledAcpRuntimeIdsSnapshot,
+} from "@/features/agents/lib/runtimeVisibilityPreference";
import {
addChannelMembers,
createManagedAgent,
@@ -14,6 +18,7 @@ import { getGlobalAgentConfig } from "@/shared/api/tauriGlobalAgentConfig";
import { listPersonas, setPersonaActive } from "@/shared/api/tauriPersonas";
import type {
AcpRuntime,
+ AcpRuntimeCatalogEntry,
AgentPersona,
CreateManagedAgentInput,
ManagedAgent,
@@ -232,9 +237,28 @@ export async function buildWelcomeStarterCreateInput(
export function welcomeStarterRuntimeUpdate(
existing: ManagedAgent,
desired: CreateManagedAgentInput,
+ visibility?: {
+ runtimes: readonly AcpRuntimeCatalogEntry[];
+ disabledRuntimeIds: readonly string[];
+ },
) {
if (!desired.agentCommand) return null;
+ const existingRuntime = visibility?.runtimes.find(
+ (runtime) =>
+ runtime.command?.trim() === existing.agentCommand.trim() ||
+ runtime.id.trim() === existing.agentCommand.trim(),
+ );
+ if (
+ existingRuntime &&
+ filterEnabledAcpRuntimes(
+ [existingRuntime],
+ visibility?.disabledRuntimeIds ?? [],
+ ).length === 0
+ ) {
+ return null;
+ }
+
const desiredArgs = desired.agentArgs ?? [];
const desiredModel = desired.model ?? null;
const desiredProvider = desired.provider ?? null;
@@ -282,6 +306,7 @@ async function provisionWelcomeTeam(
const runtimes = runtimeCatalog.filter(
(runtime): runtime is AcpRuntime => runtime.availability === "available",
);
+ const disabledRuntimeIds = getDisabledAcpRuntimeIdsSnapshot();
const agents: ManagedAgent[] = [];
for (const starter of WELCOME_TEAM_STARTERS) {
@@ -302,7 +327,10 @@ async function provisionWelcomeTeam(
relayUrl,
);
if (existing) {
- const runtimeUpdate = welcomeStarterRuntimeUpdate(existing, desired);
+ const runtimeUpdate = welcomeStarterRuntimeUpdate(existing, desired, {
+ runtimes: runtimeCatalog,
+ disabledRuntimeIds,
+ });
agents.push(
runtimeUpdate
? (await updateManagedAgent(runtimeUpdate)).agent
From 0a0fba24fe4dfaee6e06973a378219bbbc89e5d3 Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Fri, 24 Jul 2026 08:24:48 -0700
Subject: [PATCH 15/22] Fix harness visibility smoke fixture
---
desktop/tests/e2e/doctor-states.spec.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/desktop/tests/e2e/doctor-states.spec.ts b/desktop/tests/e2e/doctor-states.spec.ts
index 5563d8fa96..a789fd7931 100644
--- a/desktop/tests/e2e/doctor-states.spec.ts
+++ b/desktop/tests/e2e/doctor-states.spec.ts
@@ -248,7 +248,7 @@ test.describe("Doctor panel state screenshots", () => {
await expect(defaultHarness).toHaveText("Buzz Agent");
const provider = page.getByTestId("global-agent-provider");
await provider.click();
- await page.getByTestId("global-agent-provider-option-openai").click();
+ await page.getByTestId("global-agent-provider-option-relay-mesh").click();
await page.getByRole("button", { name: "Save defaults" }).click();
const savedConfig = await page.evaluate(async () =>
(
@@ -261,7 +261,7 @@ test.describe("Doctor panel state screenshots", () => {
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.("get_global_agent_config", null),
);
expect(savedConfig).toMatchObject({
- provider: "openai",
+ provider: "relay-mesh",
preferred_runtime: "buzz-agent",
});
@@ -269,6 +269,7 @@ test.describe("Doctor panel state screenshots", () => {
await page.getByTestId("open-agents-view").click();
await page.getByTestId("new-agent-card").click();
await page.getByRole("menuitem", { name: "Create from scratch" }).click();
+ await page.getByRole("tab", { name: "Customize for this agent" }).click();
const harnessDropdown = page.locator("#persona-runtime");
await expect(harnessDropdown).toContainText("Buzz Agent");
From 5a50252ff62b5c08406dcd6a645b0fcd77ad20f6 Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Fri, 24 Jul 2026 08:54:11 -0700
Subject: [PATCH 16/22] Use harness labels for current options
---
.../agents/ui/AgentDefinitionDialog.tsx | 3 ++-
.../agents/ui/AgentInstanceEditDialog.tsx | 5 +++--
.../agents/ui/agentConfigOptions.test.mjs | 20 +++++++++++++++++++
.../features/agents/ui/agentConfigOptions.tsx | 12 +++++++++++
4 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
index f13ed8b73b..407f91739c 100644
--- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
@@ -31,6 +31,7 @@ import {
AUTO_PROVIDER_DROPDOWN_VALUE,
CUSTOM_PROVIDER_DROPDOWN_VALUE,
computeLocalModeGate,
+ formatCurrentRuntimeOptionLabel,
formatRuntimeOptionLabel,
getDefaultPersonaRuntime,
getPersonaHiddenProviderIds,
@@ -569,7 +570,7 @@ export function AgentDefinitionDialog({
!runtimeDropdownOptions.some((option) => option.value === runtime)
) {
runtimeDropdownOptions.push({
- label: `${runtime.trim()} (current)`,
+ label: formatCurrentRuntimeOptionLabel(runtimes, runtime),
value: runtime.trim(),
});
}
diff --git a/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx b/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
index be13b557ef..6d35a6c800 100644
--- a/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
@@ -27,6 +27,7 @@ import { EditAgentAdvancedFields } from "./EditAgentAdvancedFields";
import {
AUTO_PROVIDER_DROPDOWN_VALUE,
CUSTOM_PROVIDER_DROPDOWN_VALUE,
+ formatCurrentRuntimeOptionLabel,
formatRuntimeOptionLabel,
getDefaultLlmModelLabel,
getDefaultPersonaRuntime,
@@ -229,12 +230,12 @@ export function AgentInstanceEditDialog({
!options.some((o) => o.value === selectedRuntimeId)
) {
options.push({
- label: `${selectedRuntimeId} (current)`,
+ label: formatCurrentRuntimeOptionLabel(runtimes, selectedRuntimeId),
value: selectedRuntimeId,
});
}
return options;
- }, [sortedRuntimes, selectedRuntimeId]);
+ }, [runtimes, sortedRuntimes, selectedRuntimeId]);
// Resolve the dialog-opening command as the catalog loads. Edit-state runtime
// ids mutate during selection changes and cannot identify the original state.
diff --git a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
index d2dc068f90..a93e862f96 100644
--- a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
+++ b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
+ formatCurrentRuntimeOptionLabel,
getDefaultPersonaRuntime,
getPersonaHiddenProviderIds,
getPersonaModelOptions,
@@ -34,6 +35,25 @@ function makeRuntime(
};
}
+test("formatCurrentRuntimeOptionLabel uses the catalog display label", () => {
+ const runtime = {
+ ...makeRuntime("goose"),
+ label: "Goose",
+ };
+
+ assert.equal(
+ formatCurrentRuntimeOptionLabel([runtime], "goose"),
+ "Goose (current)",
+ );
+});
+
+test("formatCurrentRuntimeOptionLabel falls back to an unknown runtime id", () => {
+ assert.equal(
+ formatCurrentRuntimeOptionLabel([], " custom-runtime "),
+ "custom-runtime (current)",
+ );
+});
+
// ── getPersonaProviderOptions — hideProviderIds ───────────────────────────────
test("getPersonaProviderOptions returns databricks v1 and v2 when hideProviderIds is empty", () => {
diff --git a/desktop/src/features/agents/ui/agentConfigOptions.tsx b/desktop/src/features/agents/ui/agentConfigOptions.tsx
index d8ceb01449..f31a20441d 100644
--- a/desktop/src/features/agents/ui/agentConfigOptions.tsx
+++ b/desktop/src/features/agents/ui/agentConfigOptions.tsx
@@ -489,6 +489,18 @@ export function formatRuntimeOptionLabel(runtime: AcpRuntimeCatalogEntry) {
return `${runtime.label}${suffix}`;
}
+export function formatCurrentRuntimeOptionLabel(
+ runtimes: readonly AcpRuntimeCatalogEntry[],
+ runtimeId: string,
+) {
+ const trimmedRuntimeId = runtimeId.trim();
+ const runtime = runtimes.find(
+ (candidate) => candidate.id === trimmedRuntimeId,
+ );
+ const label = runtime ? formatRuntimeOptionLabel(runtime) : trimmedRuntimeId;
+ return `${label} (current)`;
+}
+
function runtimeAvailabilitySortRank(
availability: AcpRuntimeCatalogEntry["availability"],
) {
From 87ebbf41a48e4d6bfe346b19d64ca03a9ed67337 Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Fri, 24 Jul 2026 09:54:59 -0700
Subject: [PATCH 17/22] Filter hidden harnesses from remaining pickers
---
.../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 7230d49424..9bf8014932 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 89932e6030..040831c289 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 90f8b22d62..0eb613e437 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 {
@@ -126,14 +127,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 b10aa19154..e3aced820c 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 51339e2afe..74c501d81a 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", "codex"];
@@ -20,8 +21,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 82680ae5a8..6e619a26e7 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 (