diff --git a/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx b/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx index 07b1716aca..0846a63fdf 100644 --- a/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx +++ b/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx @@ -372,7 +372,7 @@ export default function CreateEditRecipeModal({ return (
-
+
{/* Header */}
@@ -381,12 +381,12 @@ export default function CreateEditRecipeModal({

- {isCreateMode ? 'Create Recipe' : 'View/edit recipe'} + {isCreateMode ? 'Create Command' : 'View/edit command'}

{isCreateMode - ? 'Create a new recipe to define agent behavior and capabilities.' - : "You can edit the recipe below to change the agent's behavior in a new session."} + ? 'Create a new command to define agent behavior and capabilities.' + : "You can edit the command below to change the agent's behavior in a new session."}

@@ -401,17 +401,15 @@ export default function CreateEditRecipeModal({
{/* Content */} -
- +
+ + - {/* Schedule Configuration Section */} -
- -
{/* Deep Link Display */} {requiredFieldsAreFilled() && ( @@ -480,7 +478,7 @@ export default function CreateEditRecipeModal({ className="inline-flex items-center justify-center gap-2 px-4 py-2" > - {isSaving ? 'Saving...' : 'Save Recipe'} + {isSaving ? 'Saving...' : 'Save Command'}
diff --git a/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx b/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx index 7754c3186d..564a56b74f 100644 --- a/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx +++ b/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx @@ -1,27 +1,29 @@ import React, { useState } from 'react'; import { Parameter } from '../../../recipe'; +import { ChevronDown, ChevronUp } from 'lucide-react'; import ParameterInput from '../../parameter/ParameterInput'; -import RecipeActivityEditor from '../RecipeActivityEditor'; import JsonSchemaEditor from './JsonSchemaEditor'; import InstructionsEditor from './InstructionsEditor'; import { Button } from '../../ui/button'; import { RecipeFormApi } from './recipeFormSchema'; +import { ScheduleConfigSection, ScheduleConfig } from '../../shared/ScheduleConfigSection'; -// Type for field API to avoid linting issues - use any to bypass complex type constraints +// Type for field API to avoid linting issues // eslint-disable-next-line @typescript-eslint/no-explicit-any type FormFieldApi<_T = any> = any; interface RecipeFormFieldsProps { - // Form instance from parent form: RecipeFormApi; - - // Event handlers onTitleChange?: (value: string) => void; onDescriptionChange?: (value: string) => void; onInstructionsChange?: (value: string) => void; onPromptChange?: (value: string) => void; onJsonSchemaChange?: (value: string) => void; + // Schedule configuration props + recipeTitle?: string; + scheduleConfig?: ScheduleConfig; + onScheduleConfigChange?: (config: ScheduleConfig | null) => void; } export const extractTemplateVariables = (content: string): string[] => { @@ -31,17 +33,13 @@ export const extractTemplateVariables = (content: string): string[] => { while ((match = templateVarRegex.exec(content)) !== null) { const variable = match[1].trim(); - if (variable && !variables.includes(variable)) { - // Filter out complex variables that aren't valid parameter names - // This matches the backend logic in filter_complex_variables() const validVarRegex = /^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*$/; if (validVarRegex.test(variable)) { variables.push(variable); } } } - return variables; }; @@ -52,32 +50,30 @@ export function RecipeFormFields({ onInstructionsChange, onPromptChange, onJsonSchemaChange, + recipeTitle, + scheduleConfig, + onScheduleConfigChange, }: RecipeFormFieldsProps) { - const [showJsonSchemaEditor, setShowJsonSchemaEditor] = useState(false); + // Advanced configuration state + const [showAdvanced, setShowAdvanced] = useState(false); + + // Other states const [showInstructionsEditor, setShowInstructionsEditor] = useState(false); const [newParameterName, setNewParameterName] = useState(''); const [expandedParameters, setExpandedParameters] = useState>(new Set()); - - // Force re-render when instructions, prompt, or activities change const [_forceRender, setForceRender] = useState(0); React.useEffect(() => { return form.store.subscribe(() => { - // Force re-render when any form field changes to update parameter usage indicators setForceRender((prev) => prev + 1); }); }, [form.store]); const parseParametersFromInstructions = React.useCallback( - (instructions: string, prompt?: string, activities?: string[]): Parameter[] => { + (instructions: string, prompt?: string): Parameter[] => { const instructionVars = extractTemplateVariables(instructions); const promptVars = prompt ? extractTemplateVariables(prompt) : []; - const activityVars = activities - ? activities.flatMap((activity) => extractTemplateVariables(activity)) - : []; - - // Combine and deduplicate - const allVars = [...new Set([...instructionVars, ...promptVars, ...activityVars])]; + const allVars = [...new Set([...instructionVars, ...promptVars])]; return allVars.map((key: string) => ({ key, @@ -89,76 +85,36 @@ export function RecipeFormFields({ [] ); - // Function to update parameters based on current field values const updateParametersFromFields = React.useCallback(() => { const currentValues = form.state.values; - const { instructions, prompt, activities, parameters: currentParams } = currentValues; + const { instructions, prompt, parameters: currentParams } = currentValues; - const newParams = parseParametersFromInstructions(instructions, prompt, activities); - - // Separate manually added parameters (those not found in instructions/prompt/activities) + const newParams = parseParametersFromInstructions(instructions, prompt); const manualParams = currentParams.filter((param: Parameter) => { - // Only keep manual params that have a valid key and are not found in the parsed params - return ( - param.key && param.key.trim() && !newParams.some((newParam) => newParam.key === param.key) - ); + return !newParams.some((np) => np.key === param.key); }); - // Combine parsed parameters with manually added ones, filtering out empty ones - const combinedParams = [ - ...newParams.map((newParam) => { - const existing = currentParams.find((cp: Parameter) => cp.key === newParam.key); - return existing ? { ...existing } : newParam; - }), - ...manualParams, - ].filter((param: Parameter) => param.key && param.key.trim()) as Parameter[]; - - // Only update if parameters actually changed - const currentParamKeys = currentParams.map((p: Parameter) => p.key).sort(); - const newParamKeys = combinedParams.map((p) => p.key).sort(); - - if (JSON.stringify(currentParamKeys) !== JSON.stringify(newParamKeys)) { - form.setFieldValue('parameters', combinedParams); - } + const allParams = [...newParams, ...manualParams]; + form.setFieldValue('parameters', allParams); }, [form, parseParametersFromInstructions]); - const isParameterUsed = ( - paramKey: string, - instructions: string, - prompt?: string, - activities?: string[] - ): boolean => { - const regex = new RegExp( - `\\{\\{\\s*${paramKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\}\\}`, - 'g' - ); - const usedInInstructions = regex.test(instructions); - const usedInPrompt = prompt ? regex.test(prompt) : false; - const usedInActivities = activities - ? activities.some((activity) => { - // For activities, we need to check the full activity string, including message: prefixes - return regex.test(activity); - }) - : false; - return usedInInstructions || usedInPrompt || usedInActivities; - }; + React.useEffect(() => { + updateParametersFromFields(); + }, [updateParametersFromFields]); return ( -
- {/* Title Field */} +
+ {/* REQUIRED: Title Field */} {(field: FormFieldApi) => (
-