From 0b11d5545d18e6d851358f4df132ac4c128594cb Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Tue, 23 Sep 2025 10:36:42 -0700 Subject: [PATCH 1/2] add recipe parameter value saving and substitution to resumed sessions with recipes --- crates/goose-server/src/openapi.rs | 2 + crates/goose-server/src/routes/agent.rs | 1 + crates/goose-server/src/routes/session.rs | 51 ++++++++++++++++ crates/goose/src/scheduler.rs | 1 + crates/goose/src/session/storage.rs | 5 ++ ui/desktop/openapi.json | 71 +++++++++++++++++++++++ ui/desktop/src/api/sdk.gen.ts | 13 ++++- ui/desktop/src/api/types.gen.ts | 49 ++++++++++++++++ ui/desktop/src/hooks/useAgent.ts | 4 ++ ui/desktop/src/hooks/useRecipeManager.ts | 16 ++++- ui/desktop/src/utils/providerUtils.ts | 70 +++++++++++++++++++--- 11 files changed, 273 insertions(+), 10 deletions(-) diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index c677d03796fc..53015d940432 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -385,6 +385,7 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema { super::routes::context::manage_context, super::routes::session::list_sessions, super::routes::session::get_session_history, + super::routes::session::update_session_recipe_parameters, super::routes::schedule::create_schedule, super::routes::schedule::list_schedules, super::routes::schedule::delete_schedule, @@ -420,6 +421,7 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema { super::routes::context::ContextManageResponse, super::routes::session::SessionListResponse, super::routes::session::SessionHistoryResponse, + super::routes::session::UpdateSessionRecipeParametersRequest, Message, MessageContent, MessageMetadata, diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 2a0ffbdf32d3..b4470042b23d 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -134,6 +134,7 @@ async fn start_agent( accumulated_output_tokens: Some(0), extension_data: Default::default(), recipe: payload.recipe, + recipe_parameters: None, }; let session_path = match session::get_path(session::Identifier::Name(session_id.clone())) { diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index c8d987c7431c..b3dcf989c087 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -42,6 +42,13 @@ pub struct UpdateSessionMetadataRequest { description: String, } +#[derive(Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateSessionRecipeParametersRequest { + /// Recipe parameter values entered by the user + recipe_parameters: HashMap, +} + const MAX_DESCRIPTION_LENGTH: usize = 200; #[derive(Serialize, ToSchema, Debug)] @@ -299,6 +306,46 @@ async fn update_session_metadata( Ok(StatusCode::OK) } +#[utoipa::path( + put, + path = "/sessions/{session_id}/recipe_parameters", + request_body = UpdateSessionRecipeParametersRequest, + params( + ("session_id" = String, Path, description = "Unique identifier for the session") + ), + responses( + (status = 200, description = "Session recipe parameters updated successfully"), + (status = 401, description = "Unauthorized - Invalid or missing API key"), + (status = 404, description = "Session not found"), + (status = 500, description = "Internal server error") + ), + security( + ("api_key" = []) + ), + tag = "Session Management" +)] +// Update session recipe parameters +async fn update_session_recipe_parameters( + Path(session_id): Path, + Json(request): Json, +) -> Result { + let session_path = session::get_path(session::Identifier::Name(session_id.clone())) + .map_err(|_| StatusCode::BAD_REQUEST)?; + + // Read current metadata + let mut metadata = session::read_metadata(&session_path).map_err(|_| StatusCode::NOT_FOUND)?; + + // Update recipe parameters + metadata.recipe_parameters = Some(request.recipe_parameters); + + // Save updated metadata + session::update_metadata(&session_path, &metadata) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(StatusCode::OK) +} + #[utoipa::path( delete, path = "/sessions/{session_id}/delete", @@ -346,6 +393,10 @@ pub fn routes(state: Arc) -> Router { "/sessions/{session_id}/metadata", put(update_session_metadata), ) + .route( + "/sessions/{session_id}/recipe_parameters", + put(update_session_recipe_parameters), + ) .with_state(state) } diff --git a/crates/goose/src/scheduler.rs b/crates/goose/src/scheduler.rs index 48b81d3e36cf..845123782b0b 100644 --- a/crates/goose/src/scheduler.rs +++ b/crates/goose/src/scheduler.rs @@ -1300,6 +1300,7 @@ async fn run_scheduled_job_internal( accumulated_output_tokens: None, extension_data: crate::session::ExtensionData::new(), recipe: None, + recipe_parameters: None, }; if let Err(e_fb) = crate::session::storage::save_messages_with_metadata( &session_file_path, diff --git a/crates/goose/src/session/storage.rs b/crates/goose/src/session/storage.rs index 66f761469198..d105405c858f 100644 --- a/crates/goose/src/session/storage.rs +++ b/crates/goose/src/session/storage.rs @@ -72,6 +72,8 @@ pub struct SessionMetadata { pub extension_data: ExtensionData, pub recipe: Option, + /// Recipe parameter values entered by the user + pub recipe_parameters: Option>, } // Custom deserializer to handle old sessions without working_dir @@ -95,6 +97,7 @@ impl<'de> Deserialize<'de> for SessionMetadata { #[serde(default)] extension_data: ExtensionData, recipe: Option, + recipe_parameters: Option>, } let helper = Helper::deserialize(deserializer)?; @@ -118,6 +121,7 @@ impl<'de> Deserialize<'de> for SessionMetadata { working_dir, extension_data: helper.extension_data, recipe: helper.recipe, + recipe_parameters: helper.recipe_parameters, }) } } @@ -144,6 +148,7 @@ impl SessionMetadata { accumulated_output_tokens: None, extension_data: ExtensionData::new(), recipe: None, + recipe_parameters: None, } } } diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 20df70340887..e927af8b6333 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1545,6 +1545,54 @@ ] } }, + "/sessions/{session_id}/recipe_parameters": { + "put": { + "tags": [ + "Session Management" + ], + "operationId": "update_session_recipe_parameters", + "parameters": [ + { + "name": "session_id", + "in": "path", + "description": "Unique identifier for the session", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSessionRecipeParametersRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Session recipe parameters updated successfully" + }, + "401": { + "description": "Unauthorized - Invalid or missing API key" + }, + "404": { + "description": "Session not found" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/status": { "get": { "tags": [ @@ -3601,6 +3649,14 @@ ], "nullable": true }, + "recipe_parameters": { + "type": "object", + "description": "Recipe parameter values entered by the user", + "additionalProperties": { + "type": "string" + }, + "nullable": true + }, "schedule_id": { "type": "string", "description": "ID of the schedule that triggered this session, if any", @@ -4002,6 +4058,21 @@ } } }, + "UpdateSessionRecipeParametersRequest": { + "type": "object", + "required": [ + "recipeParameters" + ], + "properties": { + "recipeParameters": { + "type": "object", + "description": "Recipe parameter values entered by the user", + "additionalProperties": { + "type": "string" + } + } + } + }, "UpsertConfigQuery": { "type": "object", "required": [ diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 39ac4c781667..1e412c4e0a71 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from './client'; -import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, ResumeAgentData, ResumeAgentResponses, ResumeAgentErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, StartAgentData, StartAgentResponses, StartAgentErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, GetProviderModelsData, GetProviderModelsResponses, GetProviderModelsErrors, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, DeleteRecipeData, DeleteRecipeResponses, DeleteRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ListRecipesData, ListRecipesResponses, ListRecipesErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors, StatusData, StatusResponses } from './types.gen'; +import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, ResumeAgentData, ResumeAgentResponses, ResumeAgentErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, StartAgentData, StartAgentResponses, StartAgentErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, GetProviderModelsData, GetProviderModelsResponses, GetProviderModelsErrors, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, DeleteRecipeData, DeleteRecipeResponses, DeleteRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ListRecipesData, ListRecipesResponses, ListRecipesErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors, UpdateSessionRecipeParametersData, UpdateSessionRecipeParametersResponses, UpdateSessionRecipeParametersErrors, StatusData, StatusResponses } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -431,6 +431,17 @@ export const getSessionHistory = (options: }); }; +export const updateSessionRecipeParameters = (options: Options) => { + return (options.client ?? _heyApiClient).put({ + url: '/sessions/{session_id}/recipe_parameters', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + export const status = (options?: Options) => { return (options?.client ?? _heyApiClient).get({ url: '/status', diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 14c1162ffe02..d9daf56ff552 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -770,6 +770,12 @@ export type SessionMetadata = { */ output_tokens?: number | null; recipe?: Recipe | null; + /** + * Recipe parameter values entered by the user + */ + recipe_parameters?: { + [key: string]: string; + } | null; /** * ID of the schedule that triggered this session, if any */ @@ -922,6 +928,15 @@ export type UpdateScheduleRequest = { cron: string; }; +export type UpdateSessionRecipeParametersRequest = { + /** + * Recipe parameter values entered by the user + */ + recipeParameters: { + [key: string]: string; + }; +}; + export type UpsertConfigQuery = { is_secret: boolean; key: string; @@ -2172,6 +2187,40 @@ export type GetSessionHistoryResponses = { export type GetSessionHistoryResponse = GetSessionHistoryResponses[keyof GetSessionHistoryResponses]; +export type UpdateSessionRecipeParametersData = { + body: UpdateSessionRecipeParametersRequest; + path: { + /** + * Unique identifier for the session + */ + session_id: string; + }; + query?: never; + url: '/sessions/{session_id}/recipe_parameters'; +}; + +export type UpdateSessionRecipeParametersErrors = { + /** + * Unauthorized - Invalid or missing API key + */ + 401: unknown; + /** + * Session not found + */ + 404: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type UpdateSessionRecipeParametersResponses = { + /** + * Session recipe parameters updated successfully + */ + 200: unknown; +}; + export type StatusData = { body?: never; path?: never; diff --git a/ui/desktop/src/hooks/useAgent.ts b/ui/desktop/src/hooks/useAgent.ts index 354c07d0c857..819b39144ffb 100644 --- a/ui/desktop/src/hooks/useAgent.ts +++ b/ui/desktop/src/hooks/useAgent.ts @@ -90,6 +90,7 @@ export function useAgent(): UseAgentReturn { convertApiMessageToFrontendMessage(message) ), recipeConfig: sessionMetadata.recipe, + recipeParameters: sessionMetadata.recipe_parameters || null, }; return chat; @@ -151,6 +152,8 @@ export function useAgent(): UseAgentReturn { getExtensions, addExtension, setIsExtensionsLoading: initContext.setIsExtensionsLoading, + recipeParameters: agentSessionInfo.metadata.recipe_parameters, + recipeConfig: initContext.recipeConfig || agentSessionInfo.metadata.recipe || undefined, }); if (COST_TRACKING_ENABLED) { @@ -180,6 +183,7 @@ export function useAgent(): UseAgentReturn { messageHistoryIndex: 0, messages: messages, recipeConfig: recipeConfig, + recipeParameters: sessionMetadata.recipe_parameters || null, }; setAgentState(AgentState.INITIALIZED); diff --git a/ui/desktop/src/hooks/useRecipeManager.ts b/ui/desktop/src/hooks/useRecipeManager.ts index 18f7853f86e6..42a5d83352f0 100644 --- a/ui/desktop/src/hooks/useRecipeManager.ts +++ b/ui/desktop/src/hooks/useRecipeManager.ts @@ -5,6 +5,7 @@ import { updateSystemPromptWithParameters, substituteParameters, filterValidUsedParameters, + updateSessionMetadataWithParameters, } from '../utils/providerUtils'; import { useChatContext } from '../contexts/ChatContext'; import { ChatType } from '../types/chat'; @@ -71,6 +72,16 @@ export const useRecipeManager = (chat: ChatType, recipeConfig?: Recipe | null) = useEffect(() => { const checkRecipeAcceptance = async () => { if (finalRecipeConfig) { + // If the recipe comes from session metadata (not from navigation state), + // it means it was already accepted in a previous session, so auto-accept it + const isFromSessionMetadata = !recipeConfig && finalRecipeConfig; + + if (isFromSessionMetadata) { + // Recipe loaded from session metadata should be automatically accepted + setRecipeAccepted(true); + return; + } + try { const hasAccepted = await window.electron.hasAcceptedRecipeBefore(finalRecipeConfig); @@ -93,7 +104,7 @@ export const useRecipeManager = (chat: ChatType, recipeConfig?: Recipe | null) = }; checkRecipeAcceptance(); - }, [finalRecipeConfig]); + }, [finalRecipeConfig, recipeConfig]); // Filter parameters to only show valid ones that are actually used in the recipe const filteredParameters = useMemo(() => { @@ -189,6 +200,9 @@ export const useRecipeManager = (chat: ChatType, recipeConfig?: Recipe | null) = inputValues, finalRecipeConfig || undefined ); + + // Save recipe parameters to session metadata + await updateSessionMetadataWithParameters(chat.sessionId, inputValues); } catch (error) { console.error('Failed to update system prompt with parameters:', error); } diff --git a/ui/desktop/src/utils/providerUtils.ts b/ui/desktop/src/utils/providerUtils.ts index 703196df481c..8f9b0d1232bd 100644 --- a/ui/desktop/src/utils/providerUtils.ts +++ b/ui/desktop/src/utils/providerUtils.ts @@ -8,6 +8,7 @@ import type { ExtensionConfig, FixedExtensionEntry } from '../components/ConfigC import { addSubRecipesToAgent } from '../recipe/add_sub_recipe_on_agent'; import { extendPrompt, + Recipe, RecipeParameter, SubRecipe, updateAgentProvider, @@ -204,6 +205,8 @@ export const initializeSystem = async ( getExtensions?: (b: boolean) => Promise; addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise; setIsExtensionsLoading?: (loading: boolean) => void; + recipeParameters?: Record | null; + recipeConfig?: Recipe; } ) => { try { @@ -228,18 +231,26 @@ export const initializeSystem = async ( console.log('This will not end well'); } - // Get recipeConfig directly here - const recipeConfig = window.appConfig?.get?.('recipe'); + // Get recipeConfig - prefer from options (session metadata) over app config + const recipeConfig = options?.recipeConfig || window.appConfig?.get?.('recipe'); const recipe_instructions = (recipeConfig as { instructions?: string })?.instructions; const responseConfig = (recipeConfig as { response?: { json_schema?: unknown } })?.response; const subRecipes = (recipeConfig as { sub_recipes?: SubRecipe[] })?.sub_recipes; - const parameters = (recipeConfig as { parameters?: RecipeParameter[] })?.parameters; - const hasParameters = parameters && parameters?.length > 0; const hasSubRecipes = subRecipes && subRecipes?.length > 0; + const recipeParameters = options?.recipeParameters; + + // Determine the system prompt let prompt = desktopPrompt; - if (!hasParameters && recipe_instructions) { - prompt = `${desktopPromptBot}\nIMPORTANT instructions for you to operate as agent:\n${recipe_instructions}`; + + // If we have recipe instructions, add them to the system prompt with parameter substitution + if (recipe_instructions) { + const substitutedInstructions = recipeParameters + ? substituteParameters(recipe_instructions, recipeParameters) + : recipe_instructions; + + prompt = `${desktopPromptBot}\nIMPORTANT instructions for you to operate as agent:\n${substitutedInstructions}`; } + // Extend the system prompt with desktop-specific information await extendPrompt({ body: { @@ -248,9 +259,27 @@ export const initializeSystem = async ( }, }); - if (!hasParameters && hasSubRecipes) { - await addSubRecipesToAgent(sessionId, subRecipes); + if (hasSubRecipes) { + let finalSubRecipes = subRecipes; + + // If we have parameters, substitute them in sub-recipe values + if (recipeParameters) { + finalSubRecipes = subRecipes.map((subRecipe) => ({ + ...subRecipe, + values: subRecipe.values + ? Object.fromEntries( + Object.entries(subRecipe.values).map(([key, value]) => [ + key, + substituteParameters(value, recipeParameters), + ]) + ) + : subRecipe.values, + })); + } + + await addSubRecipesToAgent(sessionId, finalSubRecipes); } + // Configure session with response config if present if (responseConfig?.json_schema) { const sessionConfigResponse = await updateSessionConfig({ @@ -307,3 +336,28 @@ export const initializeSystem = async ( throw error; } }; + +/** + * Updates session metadata with recipe parameters + * This ensures parameters are persisted across session resumption + */ +export const updateSessionMetadataWithParameters = async ( + sessionId: string, + recipeParameters: Record +): Promise => { + try { + const { updateSessionRecipeParameters } = await import('../api'); + + await updateSessionRecipeParameters({ + path: { + session_id: sessionId, + }, + body: { + recipeParameters, + }, + throwOnError: true, + }); + } catch (error) { + console.error('Failed to save recipe parameters to session metadata:', error); + } +}; From 67eada4c2fee29e0f4c910276b54d6526566ff55 Mon Sep 17 00:00:00 2001 From: Zane <75694352+zanesq@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:47:49 -0700 Subject: [PATCH 2/2] Add tests for create / edit recipe (#4784) --- .../recipes/CreateRecipeFromSessionModal.tsx | 61 +- .../recipes/RecipeActivityEditor.tsx | 4 +- .../components/recipes/ViewRecipeModal.tsx | 2 +- .../CreateRecipeFromSessionModal.test.tsx | 416 ++++++++++ .../recipes/shared/RecipeFormFields.tsx | 40 +- .../recipes/shared/RecipeNameField.tsx | 1 + .../__tests__/RecipeActivityEditor.test.tsx | 136 ++++ .../__tests__/RecipeFormFields.test.tsx | 744 ++++++++++++++++++ .../shared/__tests__/recipeFormSchema.test.ts | 430 ++++++++++ 9 files changed, 1801 insertions(+), 33 deletions(-) create mode 100644 ui/desktop/src/components/recipes/__tests__/CreateRecipeFromSessionModal.test.tsx create mode 100644 ui/desktop/src/components/recipes/shared/__tests__/RecipeActivityEditor.test.tsx create mode 100644 ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx create mode 100644 ui/desktop/src/components/recipes/shared/__tests__/recipeFormSchema.test.ts diff --git a/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx b/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx index 92f44d009600..7f95342f7aea 100644 --- a/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx +++ b/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx @@ -4,7 +4,7 @@ import { Recipe } from '../../recipe'; import { Geese } from '../icons/Geese'; import { X, Save, Play, Loader2 } from 'lucide-react'; import { Button } from '../ui/button'; -import RecipeFormFields from './shared/RecipeFormFields'; +import { RecipeFormFields } from './shared/RecipeFormFields'; import { RecipeFormData } from './shared/recipeFormSchema'; import { createRecipe } from '../../api/sdk.gen'; import { toastError } from '../../toasts'; @@ -227,10 +227,16 @@ export default function CreateRecipeFromSessionModal({ if (!isOpen) return null; return ( -
+
{/* Header */} -
+
@@ -247,43 +253,61 @@ export default function CreateRecipeFromSessionModal({ variant="ghost" size="sm" className="p-2 hover:bg-bgSubtle rounded-lg transition-colors" + data-testid="close-button" >
{/* Content */} -
+
{isAnalyzing ? ( -
+
- -
+ +
Analyzing your conversation...
-
{analysisStage}
+
+ {analysisStage} +
Extracting insights from your chat
) : ( - +
+ +
)}
{/* Footer */} -
+
@@ -292,22 +316,24 @@ export default function CreateRecipeFromSessionModal({ {isAnalyzing ? (
) : createdRecipe ? ( - <> +
- +
) : (