diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md index 7a226b8a5..54361cf66 100644 --- a/desktop/src/features/agents/AGENTS.md +++ b/desktop/src/features/agents/AGENTS.md @@ -96,9 +96,11 @@ 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. - Runtime-less agent starts and team deploys must filter through the same - visible-runtime set; definitions already pinned to a hidden runtime remain - runnable. + `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. Definitions already pinned to a hidden runtime + remain runnable. 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/lib/instanceInputForDefinition.test.mjs b/desktop/src/features/agents/lib/instanceInputForDefinition.test.mjs index 4496b01ce..a4588420e 100644 --- a/desktop/src/features/agents/lib/instanceInputForDefinition.test.mjs +++ b/desktop/src/features/agents/lib/instanceInputForDefinition.test.mjs @@ -331,6 +331,28 @@ test("item-13: goose-only available — persona with no runtime resolves goose", assert.deepEqual(warnings, []); }); +test("runtime-less starts exclude disabled runtimes at the shared resolver", () => { + const { runtime, warnings } = resolveStartRuntimeForDefinition( + persona({ runtime: undefined }), + [gooseRuntime, buzzAgentRuntime], + "buzz-agent", + ["buzz-agent"], + ); + assert.equal(runtime.id, "goose"); + assert.deepEqual(warnings, []); +}); + +test("explicit definitions can still start on a hidden runtime", () => { + const { runtime, warnings } = resolveStartRuntimeForDefinition( + persona({ runtime: "buzz-agent" }), + [gooseRuntime, buzzAgentRuntime], + null, + ["buzz-agent"], + ); + assert.equal(runtime.id, "buzz-agent"); + assert.deepEqual(warnings, []); +}); + test("item-13: no runtimes available — refuses with actionable error", () => { assert.throws( () => resolveStartRuntimeForDefinition(persona({ runtime: undefined }), []), diff --git a/desktop/src/features/agents/lib/instanceInputForDefinition.ts b/desktop/src/features/agents/lib/instanceInputForDefinition.ts index e5012e467..73b24d165 100644 --- a/desktop/src/features/agents/lib/instanceInputForDefinition.ts +++ b/desktop/src/features/agents/lib/instanceInputForDefinition.ts @@ -9,6 +9,10 @@ import { resolvePersonaRuntime, type ResolvePersonaRuntimeResult, } from "./resolvePersonaRuntime"; +import { + getDisabledAcpRuntimeIdsSnapshot, + runtimesForImplicitAcpSelection, +} from "./runtimeVisibilityPreference"; import { resolveManagedAgentAvatarUrl, type UploadMediaBytes, @@ -46,13 +50,22 @@ export function resolveStartRuntimeForDefinition( persona: AgentPersona, runtimes: readonly AcpRuntime[], preferredRuntimeId?: string | null, + disabledRuntimeIds: readonly string[] = getDisabledAcpRuntimeIdsSnapshot(), ): { runtime: AcpRuntime; warnings: string[] } { + const selectableRuntimes = runtimesForImplicitAcpSelection( + runtimes, + disabledRuntimeIds, + persona.runtime, + ); // Use the buzz-agent-first preference (buzz-agent → goose → first available) // so a freshly installed goose never beats the bundled buzz-agent sidecar // for runtime-less personas (item 13 regression guard). - const defaultRuntime = getDefaultPersonaRuntime(runtimes, preferredRuntimeId); + const defaultRuntime = getDefaultPersonaRuntime( + selectableRuntimes, + preferredRuntimeId, + ); const { runtime, warnings, isOverridden }: ResolvePersonaRuntimeResult = - resolvePersonaRuntime(persona.runtime, runtimes, defaultRuntime); + resolvePersonaRuntime(persona.runtime, selectableRuntimes, defaultRuntime); if (!runtime) { throw new Error("No available runtime found for this agent."); diff --git a/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts b/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts index 361259b39..071176ead 100644 --- a/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts +++ b/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts @@ -143,7 +143,7 @@ export function maskDisabledAcpRuntimePreference< }; } -function getDisabledRuntimeIdsSnapshot(): readonly string[] { +export function getDisabledAcpRuntimeIdsSnapshot(): readonly string[] { const storage = getLocalStorage(); if (!storage) return EMPTY_DISABLED_RUNTIME_IDS; @@ -191,7 +191,7 @@ export function setAcpRuntimeEnabled( const storage = getLocalStorage(); if (!storage) return false; - const current = getDisabledRuntimeIdsSnapshot(); + const current = getDisabledAcpRuntimeIdsSnapshot(); const next = nextDisabledAcpRuntimeIds(current, runtimeId, enabled); if (next === current) return true; @@ -211,7 +211,7 @@ export function setAcpRuntimeEnabled( export function useDisabledAcpRuntimeIds(): readonly string[] { return React.useSyncExternalStore( subscribeToRuntimeVisibility, - getDisabledRuntimeIdsSnapshot, + getDisabledAcpRuntimeIdsSnapshot, () => EMPTY_DISABLED_RUNTIME_IDS, ); } diff --git a/desktop/src/features/agents/ui/useManagedAgentActions.ts b/desktop/src/features/agents/ui/useManagedAgentActions.ts index b4f216dc8..e1c2e9c9f 100644 --- a/desktop/src/features/agents/ui/useManagedAgentActions.ts +++ b/desktop/src/features/agents/ui/useManagedAgentActions.ts @@ -35,14 +35,9 @@ import { buildInstanceInputForDefinition, resolveStartRuntimeForDefinition, } from "../lib/instanceInputForDefinition"; -import { - runtimesForImplicitAcpSelection, - useDisabledAcpRuntimeIds, -} from "../lib/runtimeVisibilityPreference"; export function useManagedAgentActions() { const { globalConfig } = useGlobalAgentConfig(); - const disabledRuntimeIds = useDisabledAcpRuntimeIds(); const relayAgentsQuery = useRelayAgentsQuery(); const managedAgentsQuery = useManagedAgentsQuery(); const [shouldLoadChannels, setShouldLoadChannels] = React.useState(false); @@ -197,11 +192,7 @@ export function useManagedAgentActions() { setPersonaStartPending(persona.id, true); clearFeedback(); try { - const runtimes = runtimesForImplicitAcpSelection( - await availableRuntimesForStart(availableRuntimesQuery), - disabledRuntimeIds, - persona.runtime, - ); + const runtimes = await availableRuntimesForStart(availableRuntimesQuery); const { runtime, warnings } = resolveStartRuntimeForDefinition( persona, runtimes,