From ce09fd918cf6c607a4d9905f5993f99726196449 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Wed, 28 Jan 2026 09:28:31 -0800 Subject: [PATCH 1/3] use UI enabled extensions in recipes also --- crates/goose-server/src/routes/agent.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 588a2dfb5cef..8e69d3c37202 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -230,11 +230,18 @@ async fn start_agent( } })?; - let recipe_extensions = original_recipe - .as_ref() - .and_then(|r| r.extensions.as_deref()); - let extensions_to_use = - resolve_extensions_for_new_session(recipe_extensions, extension_overrides); + let mut extensions_to_use = resolve_extensions_for_new_session(None, extension_overrides); + if let Some(recipe_extensions) = original_recipe.as_ref().and_then(|r| r.extensions.as_ref()) { + for recipe_ext in recipe_extensions { + let recipe_ext_name = recipe_ext.name(); + if !extensions_to_use + .iter() + .any(|e| e.name() == recipe_ext_name) + { + extensions_to_use.push(recipe_ext.clone()); + } + } + } let mut extension_data = session.extension_data.clone(); let extensions_state = EnabledExtensionsState::new(extensions_to_use); if let Err(e) = extensions_state.to_extension_data(&mut extension_data) { From 68732d8bcd74cf6c55af4a48644da72fdf56e23d Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Wed, 28 Jan 2026 14:57:20 -0800 Subject: [PATCH 2/3] strip empty extensions when saving and loading recipes --- crates/goose-server/src/routes/agent.rs | 17 +++------- .../recipes/CreateEditRecipeModal.tsx | 18 ++++++---- .../recipes/CreateRecipeFromSessionModal.tsx | 2 +- .../src/components/recipes/RecipesView.tsx | 8 ++--- .../src/components/schedule/ScheduleModal.tsx | 33 ++++++++++--------- ui/desktop/src/recipe/index.ts | 13 ++++++++ 6 files changed, 52 insertions(+), 39 deletions(-) diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 8e69d3c37202..588a2dfb5cef 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -230,18 +230,11 @@ async fn start_agent( } })?; - let mut extensions_to_use = resolve_extensions_for_new_session(None, extension_overrides); - if let Some(recipe_extensions) = original_recipe.as_ref().and_then(|r| r.extensions.as_ref()) { - for recipe_ext in recipe_extensions { - let recipe_ext_name = recipe_ext.name(); - if !extensions_to_use - .iter() - .any(|e| e.name() == recipe_ext_name) - { - extensions_to_use.push(recipe_ext.clone()); - } - } - } + let recipe_extensions = original_recipe + .as_ref() + .and_then(|r| r.extensions.as_deref()); + let extensions_to_use = + resolve_extensions_for_new_session(recipe_extensions, extension_overrides); let mut extension_data = session.extension_data.clone(); let extensions_state = EnabledExtensionsState::new(extensions_to_use); if let Err(e) = extensions_state.to_extension_data(&mut extension_data) { diff --git a/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx b/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx index 5f342e06ece1..20605afec70b 100644 --- a/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx +++ b/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useForm } from '@tanstack/react-form'; -import { Recipe, generateDeepLink, Parameter } from '../../recipe'; +import { Recipe, generateDeepLink, Parameter, stripEmptyExtensions } from '../../recipe'; import { Check, ExternalLink, Play, Save, X } from 'lucide-react'; import { Geese } from '../icons/Geese'; import Copy from '../icons/Copy'; @@ -132,7 +132,14 @@ export default function CreateEditRecipeModal({ } } - return { + // Strip envs to avoid leaking secrets + const extensionsWithoutEnvs = recipeExtensions.map((extension) => + 'envs' in extension ? { ...extension, envs: undefined } : extension + ) as ExtensionConfig[]; + + // Use stripEmptyExtensions to avoid saving empty extensions array + // Empty extensions array would prevent default extensions from loading + return stripEmptyExtensions({ ...recipe, title, description, @@ -141,11 +148,8 @@ export default function CreateEditRecipeModal({ prompt: prompt || undefined, parameters: formattedParameters, response: responseConfig, - // Strip envs to avoid leaking secrets - extensions: recipeExtensions.map((extension) => - 'envs' in extension ? { ...extension, envs: undefined } : extension - ) as ExtensionConfig[], - }; + extensions: extensionsWithoutEnvs, + }) as Recipe; }, [ recipe, title, diff --git a/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx b/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx index a16051a810d9..66509c4cf019 100644 --- a/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx +++ b/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx @@ -157,6 +157,7 @@ export default function CreateRecipeFromSessionModal({ setIsCreating(true); try { // Create the recipe object from form data + // Don't set extensions - let the recipe use default enabled extensions const recipe: Recipe = { title: formData.title, description: formData.description, @@ -181,7 +182,6 @@ export default function CreateRecipeFromSessionModal({ json_schema: JSON.parse(formData.jsonSchema), } : undefined, - extensions: [], // Will be populated based on current extensions }; let recipeId = await saveRecipe(recipe, null); diff --git a/ui/desktop/src/components/recipes/RecipesView.tsx b/ui/desktop/src/components/recipes/RecipesView.tsx index 64fe60f6d2c4..905efd17bcfa 100644 --- a/ui/desktop/src/components/recipes/RecipesView.tsx +++ b/ui/desktop/src/components/recipes/RecipesView.tsx @@ -32,7 +32,7 @@ import { } from '../../api'; import ImportRecipeForm, { ImportRecipeButton } from './ImportRecipeForm'; import CreateEditRecipeModal from './CreateEditRecipeModal'; -import { generateDeepLink, Recipe } from '../../recipe'; +import { generateDeepLink, Recipe, stripEmptyExtensions } from '../../recipe'; import { useNavigation } from '../../hooks/useNavigation'; import { CronPicker } from '../schedule/CronPicker'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog'; @@ -138,12 +138,12 @@ export default function RecipesView() { } }; - const handleStartRecipeChat = async (recipe: Recipe, _recipeId: string) => { + const handleStartRecipeChat = async (recipe: Recipe) => { try { const newAgent = await startAgent({ body: { working_dir: getInitialWorkingDir(), - recipe, + recipe: stripEmptyExtensions(recipe) as Recipe, }, throwOnError: true, }); @@ -506,7 +506,7 @@ export default function RecipesView() {