From d50fa08b8a5100e54f2cfe6e3cd6df7ca27f0e2f Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Mon, 8 Dec 2025 10:47:55 -0800 Subject: [PATCH 1/4] hide advanced recipe options under expandable content --- ui/desktop/src/components/ChatInput.tsx | 2 +- .../recipes/shared/RecipeFormFields.tsx | 385 ++++++++++-------- .../__tests__/RecipeFormFields.test.tsx | 42 +- 3 files changed, 249 insertions(+), 180 deletions(-) diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index ecf8fe11702b..3b972dc92ef5 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -1507,7 +1507,7 @@ export default function ChatInput({ )} - {sessionId && ( + {sessionId && messages.length > 0 && ( <>
diff --git a/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx b/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx index 7754c3186d5e..d4031ab25837 100644 --- a/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx +++ b/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx @@ -1,12 +1,14 @@ import React, { useState } from 'react'; import { Parameter } from '../../../recipe'; +import { ChevronDown } 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 { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../ui/collapsible'; +import { RecipeFormApi, RecipeFormData } from './recipeFormSchema'; // Type for field API to avoid linting issues - use any to bypass complex type constraints // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -143,6 +145,15 @@ export function RecipeFormFields({ return usedInInstructions || usedInPrompt || usedInActivities; }; + const checkHasAdvancedData = React.useCallback((values: RecipeFormData) => { + const hasActivities = Boolean(values.activities && values.activities.length > 0); + const hasParameters = Boolean(values.parameters && values.parameters.length > 0); + const hasJsonSchema = Boolean(values.jsonSchema && values.jsonSchema.trim()); + return hasActivities || hasParameters || hasJsonSchema; + }, []); + + const [advancedOpen, setAdvancedOpen] = useState(() => checkHasAdvancedData(form.state.values)); + return (
{/* Title Field */} @@ -249,7 +260,8 @@ export function RecipeFormFields({ data-testid="instructions-input" />

- Use {`{{parameter_name}}`} to define parameters that users can fill in + Use {`{{parameter_name}}`} to define parameters that can be filled in when running the + recipe.

{field.state.meta.errors.length > 0 && (

{field.state.meta.errors[0]}

@@ -304,187 +316,208 @@ export function RecipeFormFields({ )} - {/* Activities Field */} - - {(field: FormFieldApi) => ( -
- field.handleChange(activities)} - onBlur={updateParametersFromFields} - /> -
- )} -
- - {/* Parameters Field */} - - {(field: FormFieldApi) => { - const handleAddParameter = () => { - if (newParameterName.trim()) { - const newParam: Parameter = { - key: newParameterName.trim(), - description: `Enter value for ${newParameterName.trim()}`, - input_type: 'string', - requirement: 'required', - }; - field.handleChange([...field.state.value, newParam]); - setNewParameterName(''); - // Expand the newly added parameter by default - setExpandedParameters((prev) => { - const newSet = new Set(prev); - newSet.add(newParam.key); - return newSet; - }); - } - }; - - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleAddParameter(); - } - }; - - const handleDeleteParameter = (parameterKey: string) => { - const updatedParams = field.state.value.filter( - (param: Parameter) => param.key !== parameterKey - ); - field.handleChange(updatedParams); - // Remove from expanded set if it was expanded - setExpandedParameters((prev) => { - const newSet = new Set(prev); - newSet.delete(parameterKey); - return newSet; - }); - }; - - const handleToggleExpanded = (parameterKey: string) => { - setExpandedParameters((prev) => { - const newSet = new Set(prev); - if (newSet.has(parameterKey)) { - newSet.delete(parameterKey); - } else { - newSet.add(parameterKey); - } - return newSet; - }); - }; - - return ( -
- -

- Parameters will be automatically detected from {`{{parameter_name}}`} syntax in - instructions/prompt/activities or you can manually add them below. -

- - {/* Add Parameter Input - Always Visible */} -
- setNewParameterName(e.target.value)} - onKeyPress={handleKeyPress} - placeholder="Enter parameter name..." - className="flex-1 px-3 py-2 border border-border-subtle rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm" + {/* Advanced Section - Collapsible */} + + + + Advanced Options + + Activities, parameters, response schema + + + + + {/* Activities Field */} + + {(field: FormFieldApi) => ( +
+ field.handleChange(activities)} + onBlur={updateParametersFromFields} /> -
+ )} +
+ + {/* Parameters Field */} + + {(field: FormFieldApi) => { + const handleAddParameter = () => { + if (newParameterName.trim()) { + const newParam: Parameter = { + key: newParameterName.trim(), + description: `Enter value for ${newParameterName.trim()}`, + input_type: 'string', + requirement: 'required', + }; + field.handleChange([...field.state.value, newParam]); + setNewParameterName(''); + // Expand the newly added parameter by default + setExpandedParameters((prev) => { + const newSet = new Set(prev); + newSet.add(newParam.key); + return newSet; + }); + } + }; - {field.state.value.length > 0 && - field.state.value - .filter((parameter: Parameter) => parameter.key && parameter.key.trim()) // Filter out empty parameters - .map((parameter: Parameter) => { - const currentValues = form.state.values; - const isUnused = !isParameterUsed( - parameter.key, - currentValues.instructions, - currentValues.prompt, - currentValues.activities - ); - - return ( - { - const updatedParams = field.state.value.map((param: Parameter) => - param.key === name ? { ...param, ...value } : param - ); - field.handleChange(updatedParams); - }} - /> - ); - })} -
- ); - }} - + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddParameter(); + } + }; - {/* JSON Schema Field */} - - {(field: FormFieldApi) => ( -
- -

- Define the expected structure of the AI's response using JSON Schema format -

-
- -
+ const handleDeleteParameter = (parameterKey: string) => { + const updatedParams = field.state.value.filter( + (param: Parameter) => param.key !== parameterKey + ); + field.handleChange(updatedParams); + // Remove from expanded set if it was expanded + setExpandedParameters((prev) => { + const newSet = new Set(prev); + newSet.delete(parameterKey); + return newSet; + }); + }; - {field.state.value && field.state.value.trim() && ( -
0 ? 'border-red-500' : 'border-border-subtle' - }`} - > -
-                  {field.state.value}
-                
-
- )} + const handleToggleExpanded = (parameterKey: string) => { + setExpandedParameters((prev) => { + const newSet = new Set(prev); + if (newSet.has(parameterKey)) { + newSet.delete(parameterKey); + } else { + newSet.add(parameterKey); + } + return newSet; + }); + }; - {field.state.meta.errors.length > 0 && ( -

{field.state.meta.errors[0]}

+ return ( +
+ +

+ Parameters will be automatically detected from {`{{parameter_name}}`} syntax in + instructions/prompt/activities or you can manually add them below. +

+ + {/* Add Parameter Input - Always Visible */} +
+ setNewParameterName(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Enter parameter name..." + className="flex-1 px-3 py-2 border border-border-subtle rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm" + /> + +
+ + {field.state.value.length > 0 && + field.state.value + .filter((parameter: Parameter) => parameter.key && parameter.key.trim()) // Filter out empty parameters + .map((parameter: Parameter) => { + const currentValues = form.state.values; + const isUnused = !isParameterUsed( + parameter.key, + currentValues.instructions, + currentValues.prompt, + currentValues.activities + ); + + return ( + { + const updatedParams = field.state.value.map((param: Parameter) => + param.key === name ? { ...param, ...value } : param + ); + field.handleChange(updatedParams); + }} + /> + ); + })} +
+ ); + }} + + + {/* JSON Schema Field */} + + {(field: FormFieldApi) => ( +
+ +

+ Define the expected structure of the AI's response using JSON Schema format +

+
+ +
+ + {field.state.value && field.state.value.trim() && ( +
0 ? 'border-red-500' : 'border-border-subtle' + }`} + > +
+                      {field.state.value}
+                    
+
+ )} + + {field.state.meta.errors.length > 0 && ( +

{field.state.meta.errors[0]}

+ )} + + {/* JSON Schema Editor Modal */} + setShowJsonSchemaEditor(false)} + value={field.state.value || ''} + onChange={(value) => { + field.handleChange(value); + onJsonSchemaChange?.(value); + }} + error={ + field.state.meta.errors.length > 0 ? field.state.meta.errors[0] : undefined + } + /> +
)} - - {/* JSON Schema Editor Modal */} - setShowJsonSchemaEditor(false)} - value={field.state.value || ''} - onChange={(value) => { - field.handleChange(value); - onJsonSchemaChange?.(value); - }} - error={field.state.meta.errors.length > 0 ? field.state.meta.errors[0] : undefined} - /> -
- )} -
+ + +
); } diff --git a/ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx b/ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx index b075dd2dcdcf..fbee2c8168d9 100644 --- a/ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx +++ b/ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx @@ -6,6 +6,14 @@ import { useForm } from '@tanstack/react-form'; import { RecipeFormFields, extractTemplateVariables } from '../RecipeFormFields'; import { type RecipeFormData } from '../recipeFormSchema'; +const expandAdvancedSection = async (user: ReturnType) => { + const advancedTrigger = screen.getByRole('button', { name: /advanced options/i }); + const activitiesField = screen.queryByText('Activities'); + if (!activitiesField) { + await user.click(advancedTrigger); + } +}; + describe('RecipeFormFields', () => { const useTestForm = (initialValues?: Partial) => { const defaultValues: RecipeFormData = { @@ -48,13 +56,19 @@ describe('RecipeFormFields', () => { expect(screen.getByLabelText(/title/i)).toBeInTheDocument(); }); - it('renders required form fields', () => { + it('renders required form fields', async () => { + const user = userEvent.setup(); render(); expect(screen.getByLabelText(/title/i)).toBeInTheDocument(); expect(screen.getByLabelText(/description/i)).toBeInTheDocument(); expect(screen.getByLabelText(/instructions/i)).toBeInTheDocument(); expect(screen.getByLabelText(/prompt/i)).toBeInTheDocument(); + + expect(screen.getByRole('button', { name: /advanced options/i })).toBeInTheDocument(); + + await expandAdvancedSection(user); + expect(screen.getAllByText(/activities/i)[0]).toBeInTheDocument(); expect(screen.getAllByText(/parameters/i)[0]).toBeInTheDocument(); expect(screen.getByText(/response json schema/i)).toBeInTheDocument(); @@ -99,9 +113,12 @@ describe('RecipeFormFields', () => { }); describe('Parameter Management', () => { - it('shows parameter input section', () => { + it('shows parameter input section', async () => { + const user = userEvent.setup(); render(); + await expandAdvancedSection(user); + expect(screen.getByPlaceholderText('Enter parameter name...')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /add parameter/i })).toBeInTheDocument(); }); @@ -110,6 +127,8 @@ describe('RecipeFormFields', () => { const user = userEvent.setup(); render(); + await expandAdvancedSection(user); + const parameterInput = screen.getByPlaceholderText('Enter parameter name...'); const addButton = screen.getByRole('button', { name: /add parameter/i }); @@ -173,6 +192,8 @@ describe('RecipeFormFields', () => { // Just verify the component doesn't crash and the text is there expect(instructionsInput).toHaveValue('Hello {{name}}, please {{action}} the {{item}}'); + await expandAdvancedSection(user); + // Check that the parameter section exists expect(screen.getByText('Parameters')).toBeInTheDocument(); expect( @@ -186,6 +207,8 @@ describe('RecipeFormFields', () => { const user = userEvent.setup(); render(); + await expandAdvancedSection(user); + // Add a manual parameter const parameterInput = screen.getByPlaceholderText('Enter parameter name...'); const addButton = screen.getByText('Add parameter'); @@ -200,9 +223,12 @@ describe('RecipeFormFields', () => { expect(parameterInput).toHaveValue(''); }); - it('shows parameter management UI', () => { + it('shows parameter management UI', async () => { + const user = userEvent.setup(); render(); + await expandAdvancedSection(user); + // Check parameter section exists expect(screen.getByText('Parameters')).toBeInTheDocument(); expect(screen.getByPlaceholderText('Enter parameter name...')).toBeInTheDocument(); @@ -216,6 +242,8 @@ describe('RecipeFormFields', () => { const user = userEvent.setup(); render(); + await expandAdvancedSection(user); + // Check that activities section exists expect(screen.getByText('Activities')).toBeInTheDocument(); expect(screen.getByText('Message')).toBeInTheDocument(); @@ -272,6 +300,8 @@ describe('RecipeFormFields', () => { // Wait a moment for the parameter detection to process await new Promise((resolve) => setTimeout(resolve, 100)); + await expandAdvancedSection(user); + // Check if parameters were detected and added // The parameters should appear as text in the parameter section const parameterSection = screen.getByText('Parameters').closest('div'); @@ -408,6 +438,8 @@ describe('RecipeFormFields', () => { const user = userEvent.setup(); render(); + await expandAdvancedSection(user); + // Add a manual parameter const parameterInput = screen.getByPlaceholderText('Enter parameter name...'); const addButton = screen.getByText('Add parameter'); @@ -617,6 +649,8 @@ describe('RecipeFormFields', () => { const user = userEvent.setup(); render(); + await expandAdvancedSection(user); + // Add a manual parameter const parameterInput = screen.getByPlaceholderText('Enter parameter name...'); const addButton = screen.getByText('Add parameter'); @@ -648,6 +682,8 @@ describe('RecipeFormFields', () => { const user = userEvent.setup(); render(); + await expandAdvancedSection(user); + // Add a parameter and test changing its type const parameterInput = screen.getByPlaceholderText('Enter parameter name...'); const addButton = screen.getByText('Add parameter'); From f8e0c90f810f258492c2d9cef48743f9436f074b Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Mon, 8 Dec 2025 10:54:26 -0800 Subject: [PATCH 2/4] align subtitle better and remove ellipsis from instructions and initial prompt --- .../components/recipes/shared/InstructionsEditor.tsx | 2 +- .../components/recipes/shared/RecipeFormFields.tsx | 12 +++++------- .../shared/__tests__/RecipeFormFields.test.tsx | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/ui/desktop/src/components/recipes/shared/InstructionsEditor.tsx b/ui/desktop/src/components/recipes/shared/InstructionsEditor.tsx index 90a908d46e52..e0c00a2fddfd 100644 --- a/ui/desktop/src/components/recipes/shared/InstructionsEditor.tsx +++ b/ui/desktop/src/components/recipes/shared/InstructionsEditor.tsx @@ -111,7 +111,7 @@ Use {{parameter_name}} syntax for any user-provided values.`; className={`w-full h-full min-h-[500px] p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none font-mono text-sm ${ error ? 'border-red-500' : 'border-border-subtle' }`} - placeholder="Detailed instructions for the AI, hidden from the user..." + placeholder="Detailed instructions for the AI, hidden from the user" /> {error &&

{error}

}
diff --git a/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx b/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx index d4031ab25837..7d96710df994 100644 --- a/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx +++ b/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx @@ -255,7 +255,7 @@ export function RecipeFormFields({ className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none font-mono text-sm ${ field.state.meta.errors.length > 0 ? 'border-red-500' : 'border-border-subtle' }`} - placeholder="Detailed instructions for the AI, hidden from the user..." + placeholder="Detailed instructions for the AI, hidden from the user" rows={8} data-testid="instructions-input" /> @@ -308,7 +308,7 @@ export function RecipeFormFields({ updateParametersFromFields(); }} className="w-full p-3 border border-border-subtle rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none" - placeholder="Pre-filled prompt when the recipe starts..." + placeholder="Pre-filled prompt when the recipe starts" rows={3} data-testid="prompt-input" /> @@ -318,16 +318,14 @@ export function RecipeFormFields({ {/* Advanced Section - Collapsible */} - + Advanced Options - - Activities, parameters, response schema - + Activities, parameters, response schema diff --git a/ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx b/ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx index fbee2c8168d9..90764b14118f 100644 --- a/ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx +++ b/ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx @@ -179,7 +179,7 @@ describe('RecipeFormFields', () => { render(); const instructionsInput = screen.getByPlaceholderText( - 'Detailed instructions for the AI, hidden from the user...' + 'Detailed instructions for the AI, hidden from the user' ); // Type instructions with template variables - use paste to avoid curly brace issues @@ -287,7 +287,7 @@ describe('RecipeFormFields', () => { render(); const instructionsInput = screen.getByPlaceholderText( - 'Detailed instructions for the AI, hidden from the user...' + 'Detailed instructions for the AI, hidden from the user' ); // Add instructions with template variables From 14cfbb4f016fe4af5336588ab9f5ce7e075c78a7 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Mon, 8 Dec 2025 10:55:14 -0800 Subject: [PATCH 3/4] remove ellipsis from recipe generation title --- .../src/components/recipes/CreateRecipeFromSessionModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx b/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx index 57d9c50c12dc..61cb735b4034 100644 --- a/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx +++ b/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx @@ -263,7 +263,7 @@ export default function CreateRecipeFromSessionModal({ className="text-lg font-medium text-textProminent" data-testid="analyzing-title" > - Analyzing your conversation... + Analyzing your conversation
From 8722b56ab4e2d7e9d964475d595829b02cca254b Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Mon, 8 Dec 2025 11:03:00 -0800 Subject: [PATCH 4/4] add link to the recipe docs --- .../components/recipes/CreateEditRecipeModal.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx b/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx index 3b589d081e4b..67cbdb543bda 100644 --- a/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx +++ b/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx @@ -1,9 +1,9 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useForm } from '@tanstack/react-form'; import { Recipe, generateDeepLink, Parameter } from '../../recipe'; +import { Check, ExternalLink, Play, Save, X } from 'lucide-react'; import { Geese } from '../icons/Geese'; import Copy from '../icons/Copy'; -import { Check, Save, X, Play } from 'lucide-react'; import { ExtensionConfig } from '../ConfigContext'; import { Button } from '../ui/button'; @@ -342,8 +342,17 @@ export default function CreateEditRecipeModal({

{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 recipe to define agent behavior and capabilities for reusable chat sessions.' + : "You can edit the recipe below to change the agent's behavior in a new session."}{' '} + + Learn more + +