From 953f0902dcd05af5c9dc2652d5d7a0e4583bc8b4 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Fri, 29 May 2026 14:00:29 -0400 Subject: [PATCH] feat: consume runtime metadata for env injection and overhaul runtime dropdown UX Replace hardcoded GOOSE_MODE injection with a metadata-driven default_env loop on KnownAcpRuntime, and inject GOOSE_MODEL via model_env_var for runtimes that don't support ACP model switching. This ensures goose agents respect PersonaRecord.model even without a persona pack. Redesign the AddChannelBotDialog runtime dropdown: defaults to "Use persona defaults" (unset) instead of auto-selecting the first runtime. Explicit selection overrides all personas. Per-persona runtime badges on chips show the effective runtime with warning styling when overridden or when a stored preference is unavailable. --- .../src-tauri/src/managed_agents/discovery.rs | 4 - .../src-tauri/src/managed_agents/runtime.rs | 1 + .../channels/ui/AddChannelBotDialog.tsx | 90 +++++++++---------- 3 files changed, 46 insertions(+), 49 deletions(-) diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index dd95a76dc4..9b8070ab6f 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -34,16 +34,12 @@ pub(crate) struct KnownAcpRuntime { /// pointing to the canonical `.agents/skills/sprout-cli`. `None` → this /// runtime reads the canonical path directly or has no skill support. pub skill_dir: Option<&'static str>, - // Phase 3: these fields are consumed by runtime.rs spawn logic to replace ad-hoc env var injection. - #[allow(dead_code)] pub supports_acp_model_switching: bool, - #[allow(dead_code)] pub model_env_var: Option<&'static str>, #[allow(dead_code)] pub provider_env_var: Option<&'static str>, #[allow(dead_code)] pub provider_locked: bool, - #[allow(dead_code)] pub default_env: &'static [(&'static str, &'static str)], } diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 937ff9c1b2..f9198b2a01 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -858,6 +858,7 @@ pub fn spawn_agent_child( } } // Enable MCP hook tools (_Stop, _PostCompact) for agents that need them. + // Uses "*" because build_mcp_servers() hard-codes the server name to "sprout-mcp". let runtime_meta = known_acp_runtime(&record.agent_command); if runtime_meta.is_some_and(|r| r.mcp_hooks) { command.env("MCP_HOOK_SERVERS", "*"); diff --git a/desktop/src/features/channels/ui/AddChannelBotDialog.tsx b/desktop/src/features/channels/ui/AddChannelBotDialog.tsx index f58945a25f..139931e632 100644 --- a/desktop/src/features/channels/ui/AddChannelBotDialog.tsx +++ b/desktop/src/features/channels/ui/AddChannelBotDialog.tsx @@ -29,16 +29,15 @@ import { DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/shared/ui/dropdown-menu"; import { coerceConfigValues, ProviderConfigFields, } from "@/features/agents/ui/ProviderConfigFields"; -import { - collectRuntimeWarnings, - resolvePersonaRuntime, -} from "@/features/agents/lib/resolvePersonaRuntime"; +import { resolvePersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime"; +import { useEffectiveRuntimes } from "@/features/channels/ui/useEffectiveRuntimes"; import { getActivePersonas } from "@/features/agents/lib/catalog"; import { getUsableTeams } from "@/features/agents/lib/teamPersonas"; import { useLastRuntime } from "@/features/agents/lib/useLastRuntime"; @@ -56,6 +55,8 @@ type AddChannelBotDialogProps = { onOpenChange: (open: boolean) => void; }; +const RUNTIME_NONE_SENTINEL = "__none__"; + function defaultBotName(runtime: AcpRuntime | null) { if (!runtime) { return ""; @@ -103,7 +104,7 @@ export function AddChannelBotDialog({ onAdded, onOpenChange, }: AddChannelBotDialogProps) { - const { lastRuntimeId, setLastRuntime } = useLastRuntime(); + const { setLastRuntime } = useLastRuntime(); const personasQuery = usePersonasQuery(); const teamsQuery = useTeamsQuery(); const inChannelPersonaIds = useInChannelPersonaIds( @@ -152,11 +153,13 @@ export function AddChannelBotDialog({ const selectedRuntime = React.useMemo( () => - providers.find((runtime) => runtime.id === selectedRuntimeId) ?? - providers[0] ?? - null, + selectedRuntimeId + ? (providers.find((runtime) => runtime.id === selectedRuntimeId) ?? + null) + : null, [providers, selectedRuntimeId], ); + const isOverrideActive = selectedRuntime !== null; const selectedPersonas = React.useMemo( () => personas.filter((persona) => selectedPersonaIds.includes(persona.id)), [personas, selectedPersonaIds], @@ -166,21 +169,18 @@ export function AddChannelBotDialog({ const reusableAgent = useReusableAgentDetection( channelId, open && channelId !== null, - selectedRuntime, + selectedRuntime ?? providers[0] ?? null, selectedPersonas, includeGeneric, customPrompt, ); - // Surface warnings when a persona's preferred runtime differs from the - // user-selected runtime. In this dialog the user explicitly picks a - // runtime via the dropdown, so the fallback is `selectedRuntime` (their - // choice), NOT `providers[0]`. This differs intentionally from - // AddTeamToChannelDialog which has no runtime selector and falls back - // to the first available runtime. - const runtimeWarnings = React.useMemo( - () => collectRuntimeWarnings(selectedPersonas, providers, selectedRuntime), - [selectedPersonas, providers, selectedRuntime], + const { runtimeWarnings, effectiveRuntimes } = useEffectiveRuntimes( + personas, + selectedPersonas, + providers, + selectedRuntime, + isOverrideActive, ); const isProviderMode = runOn !== "local"; @@ -197,19 +197,6 @@ export function AddChannelBotDialog({ ); }, [isProviderMode, probedProvider, providerConfig]); - React.useEffect(() => { - if (!open) { - return; - } - - if (!selectedRuntimeId && providers.length > 0) { - const remembered = lastRuntimeId - ? providers.find((p) => p.id === lastRuntimeId) - : null; - setSelectedRuntimeId(remembered ? remembered.id : providers[0].id); - } - }, [open, providers, selectedRuntimeId, lastRuntimeId]); - React.useEffect(() => { if (!selectedRuntime || hasEditedCustomName) { return; @@ -318,7 +305,7 @@ export function AddChannelBotDialog({ } async function handleSubmit() { - if (!selectedRuntime || selectedCount === 0) { + if (providers.length === 0 || selectedCount === 0) { return; } @@ -346,7 +333,7 @@ export function AddChannelBotDialog({ ...(includeGeneric ? [ { - runtime: selectedRuntime, + runtime: selectedRuntime ?? providers[0], name: customName, systemPrompt: customPrompt, role: "bot" as const, @@ -357,13 +344,15 @@ export function AddChannelBotDialog({ ] : []), ...selectedPersonas.map((persona) => { + const effectiveFallback = selectedRuntime ?? providers[0] ?? null; const resolved = resolvePersonaRuntime( persona.runtime, providers, - selectedRuntime, + effectiveFallback, + isOverrideActive, ); return { - runtime: resolved.runtime ?? selectedRuntime, + runtime: resolved.runtime ?? effectiveFallback ?? providers[0], name: persona.displayName, personaId: persona.id, systemPrompt: persona.systemPrompt, @@ -424,7 +413,7 @@ export function AddChannelBotDialog({ respondTo !== "allowlist" || respondToAllowlist.length > 0; const canSubmit = - selectedRuntime !== null && + (selectedRuntime !== null || providers.length > 0) && selectedCount > 0 && (!includeGeneric || customName.trim().length > 0) && respondToValid && @@ -436,9 +425,11 @@ export function AddChannelBotDialog({ const canChooseProvider = providers.length > 0 && !providersLoading && !createBotsMutation.isPending; const canToggleSelections = !createBotsMutation.isPending; - const providerTriggerLabel = providersLoading + const runtimeTriggerLabel = providersLoading ? "Loading runtimes..." - : (selectedRuntime?.label ?? "No runtimes found"); + : providers.length === 0 + ? "No runtimes found" + : (selectedRuntime?.label ?? "Use persona defaults"); const addButtonLabel = createBotsMutation.isPending ? selectedCount > 1 ? `Adding ${selectedCount}...` @@ -538,7 +529,7 @@ export function AddChannelBotDialog({ type="button" variant="ghost" > - {providerTriggerLabel} + {runtimeTriggerLabel} @@ -549,11 +540,19 @@ export function AddChannelBotDialog({ > { - setSelectedRuntimeId(id); - setLastRuntime(id); + if (id === RUNTIME_NONE_SENTINEL) { + setSelectedRuntimeId(""); + } else { + setSelectedRuntimeId(id); + setLastRuntime(id); + } }} - value={selectedRuntime?.id ?? ""} + value={selectedRuntime?.id ?? RUNTIME_NONE_SENTINEL} > + + Use persona defaults + + {providers.map((provider) => ( {provider.label} @@ -563,9 +562,9 @@ export function AddChannelBotDialog({

- {selectedPersonas.some((p) => p.runtime) - ? "Personas with a preferred runtime will use their own instead of this selection." - : "Default runtime for all deployed agents."} + {isOverrideActive + ? "All agents will use this runtime, overriding persona preferences." + : "Each persona uses its preferred runtime. Choose a runtime above to override all."}

@@ -583,6 +582,7 @@ export function AddChannelBotDialog({