From 1638eb8dfbe37b67a66cc06734864beae5398009 Mon Sep 17 00:00:00 2001
From: Amed Rodriguez
Date: Thu, 11 Sep 2025 15:43:45 -0700
Subject: [PATCH 1/7] add RecipeTitleField
---
.../recipes/shared/RecipeTitleField.tsx | 45 +++++++++++++++++++
1 file changed, 45 insertions(+)
create mode 100644 ui/desktop/src/components/recipes/shared/RecipeTitleField.tsx
diff --git a/ui/desktop/src/components/recipes/shared/RecipeTitleField.tsx b/ui/desktop/src/components/recipes/shared/RecipeTitleField.tsx
new file mode 100644
index 000000000000..52fc68cef083
--- /dev/null
+++ b/ui/desktop/src/components/recipes/shared/RecipeTitleField.tsx
@@ -0,0 +1,45 @@
+interface RecipeTitleFieldProps {
+ id: string;
+ value: string;
+ onChange: (value: string) => void;
+ onBlur: () => void;
+ errors: string[];
+ label?: string;
+ required?: boolean;
+ disabled?: boolean;
+}
+
+export function RecipeTitleField({
+ id,
+ value,
+ onChange,
+ onBlur,
+ errors,
+ label = 'Recipe Title',
+ required = true,
+ disabled = false,
+}: RecipeTitleFieldProps) {
+ return (
+
+
+
onChange(e.target.value)}
+ onBlur={onBlur}
+ disabled={disabled}
+ className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 ${
+ errors.length > 0 ? 'border-red-500' : 'border-border-subtle'
+ } ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
+ placeholder="My Recipe Title"
+ />
+
+ This will be the display name shown in your recipe library
+
+ {errors.length > 0 &&
{errors[0]}
}
+
+ );
+}
From a443acd72c8376c300c199619f3efeaf9182590a Mon Sep 17 00:00:00 2001
From: Amed Rodriguez
Date: Fri, 12 Sep 2025 10:51:06 -0700
Subject: [PATCH 2/7] ImportRecipeForm use recipeTitle field instead of
recipeName
---
.../components/recipes/ImportRecipeForm.tsx | 66 +++++++++----------
ui/desktop/src/recipe/recipeStorage.ts | 20 ++++--
2 files changed, 46 insertions(+), 40 deletions(-)
diff --git a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
index e8a36e46eb7e..a26b84185b95 100644
--- a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
+++ b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
@@ -9,8 +9,7 @@ import { saveRecipe } from '../../recipe/recipeStorage';
import * as yaml from 'yaml';
import { toastSuccess, toastError } from '../../toasts';
import { useEscapeKey } from '../../hooks/useEscapeKey';
-import { RecipeNameField, recipeNameSchema } from './shared/RecipeNameField';
-import { generateRecipeNameFromTitle } from './shared/recipeNameUtils';
+import { RecipeTitleField } from './shared/RecipeTitleField';
import {
validateRecipe,
getValidationErrorMessages,
@@ -39,7 +38,7 @@ const importRecipeSchema = z
if (!file) return true;
return file.size <= 1024 * 1024;
}, 'File is too large, max size is 1MB'),
- recipeName: recipeNameSchema,
+ recipeTitle: z.string().min(1, 'Recipe title is required'),
global: z.boolean(),
})
.refine((data) => (data.deeplink && data.deeplink.trim()) || data.yamlFile, {
@@ -102,7 +101,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
defaultValues: {
deeplink: '',
yamlFile: null as File | null,
- recipeName: '',
+ recipeTitle: '',
global: true,
},
validators: {
@@ -132,7 +131,8 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
}
await saveRecipe(recipe, {
- name: value.recipeName.trim(),
+ name: '',
+ title: value.recipeTitle.trim(),
global: value.global,
});
@@ -140,7 +140,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
importRecipeForm.reset({
deeplink: '',
yamlFile: null,
- recipeName: '',
+ recipeTitle: '',
global: true,
});
onClose();
@@ -148,7 +148,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
onSuccess();
toastSuccess({
- title: value.recipeName.trim(),
+ title: value.recipeTitle.trim(),
msg: 'Recipe imported successfully',
});
} catch (error) {
@@ -170,16 +170,16 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
importRecipeForm.reset({
deeplink: '',
yamlFile: null,
- recipeName: '',
+ recipeTitle: '',
global: true,
});
onClose();
};
- // Store reference to recipe name field for programmatic updates
- let recipeNameFieldRef: { handleChange: (value: string) => void } | null = null;
+ // Store reference to recipe title field for programmatic updates
+ let recipeTitleFieldRef: { handleChange: (value: string) => void } | null = null;
- // Auto-generate recipe name when deeplink changes
+ // Auto-populate recipe title when deeplink changes
const handleDeeplinkChange = async (
value: string,
field: { handleChange: (value: string) => void }
@@ -191,13 +191,11 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
try {
const recipe = await parseDeeplink(value.trim());
if (recipe && recipe.title) {
- const suggestedName = generateRecipeNameFromTitle(recipe.title);
-
- // Use the recipe name field's handleChange method if available
- if (recipeNameFieldRef) {
- recipeNameFieldRef.handleChange(suggestedName);
+ // Use the recipe title field's handleChange method if available
+ if (recipeTitleFieldRef) {
+ recipeTitleFieldRef.handleChange(recipe.title);
} else {
- importRecipeForm.setFieldValue('recipeName', suggestedName);
+ importRecipeForm.setFieldValue('recipeTitle', recipe.title);
}
}
} catch (error) {
@@ -205,11 +203,11 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
console.log('Could not parse deeplink for auto-suggest:', error);
}
} else {
- // Clear the recipe name when deeplink is empty
- if (recipeNameFieldRef) {
- recipeNameFieldRef.handleChange('');
+ // Clear the recipe title when deeplink is empty
+ if (recipeTitleFieldRef) {
+ recipeTitleFieldRef.handleChange('');
} else {
- importRecipeForm.setFieldValue('recipeName', '');
+ importRecipeForm.setFieldValue('recipeTitle', '');
}
}
};
@@ -222,13 +220,11 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
const fileContent = await file.text();
const recipe = await parseYamlFile(fileContent);
if (recipe.title) {
- const suggestedName = generateRecipeNameFromTitle(recipe.title);
-
- // Use the recipe name field's handleChange method if available
- if (recipeNameFieldRef) {
- recipeNameFieldRef.handleChange(suggestedName);
+ // Use the recipe title field's handleChange method if available
+ if (recipeTitleFieldRef) {
+ recipeTitleFieldRef.handleChange(recipe.title);
} else {
- importRecipeForm.setFieldValue('recipeName', suggestedName);
+ importRecipeForm.setFieldValue('recipeTitle', recipe.title);
}
}
} catch (error) {
@@ -236,11 +232,11 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
console.log('Could not parse YAML file for auto-suggest:', error);
}
} else {
- // Clear the recipe name when file is removed
- if (recipeNameFieldRef) {
- recipeNameFieldRef.handleChange('');
+ // Clear the recipe title when file is removed
+ if (recipeTitleFieldRef) {
+ recipeTitleFieldRef.handleChange('');
} else {
- importRecipeForm.setFieldValue('recipeName', '');
+ importRecipeForm.setFieldValue('recipeTitle', '');
}
}
};
@@ -383,14 +379,14 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
Ensure you review contents of YAML files before adding them to your goose interface.
-
+
{(field) => {
// Store reference to the field for programmatic updates
- recipeNameFieldRef = field;
+ recipeTitleFieldRef = field;
return (
- {
* Save a recipe to a file using IPC.
*/
export async function saveRecipe(recipe: Recipe, options: SaveRecipeOptions): Promise {
- const { name, global = true } = options;
+ const { name, title, global = true } = options;
- // Sanitize name
- const sanitizedName = sanitizeRecipeName(name);
- if (!sanitizedName) {
- throw new Error('Invalid recipe name');
+ let sanitizedName: string;
+
+ if (title) {
+ recipe.title = title.trim();
+ sanitizedName = generateRecipeFilename(recipe);
+ if (!sanitizedName) {
+ throw new Error('Invalid recipe title - cannot generate filename');
+ }
+ } else {
+ sanitizedName = sanitizeRecipeName(name);
+ if (!sanitizedName) {
+ throw new Error('Invalid recipe name');
+ }
}
const validationResult = validateRecipe(recipe);
From 1b6d6b8fd8ee0abcb16c5f903aa31e4f92cb989c Mon Sep 17 00:00:00 2001
From: Amed Rodriguez
Date: Fri, 12 Sep 2025 11:36:58 -0700
Subject: [PATCH 3/7] add title uniqueness validation
---
.../components/recipes/ImportRecipeForm.tsx | 32 +++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
index a26b84185b95..fc250a4bbeaa 100644
--- a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
+++ b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
@@ -10,6 +10,7 @@ import * as yaml from 'yaml';
import { toastSuccess, toastError } from '../../toasts';
import { useEscapeKey } from '../../hooks/useEscapeKey';
import { RecipeTitleField } from './shared/RecipeTitleField';
+import { listSavedRecipes } from '../../recipe/recipeStorage';
import {
validateRecipe,
getValidationErrorMessages,
@@ -97,6 +98,29 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
return recipe as Recipe;
};
+ const validateTitleUniqueness = async (
+ title: string,
+ isGlobal: boolean
+ ): Promise => {
+ if (!title.trim()) return undefined;
+
+ try {
+ const existingRecipes = await listSavedRecipes();
+ const titleExists = existingRecipes.some(
+ (recipe) =>
+ recipe.recipe.title?.toLowerCase() === title.toLowerCase() && recipe.isGlobal === isGlobal
+ );
+
+ if (titleExists) {
+ return `A recipe with the title "${title}" already exists in ${isGlobal ? 'global' : 'directory'} recipes`;
+ }
+ } catch (error) {
+ console.warn('Failed to validate title uniqueness:', error);
+ }
+
+ return undefined;
+ };
+
const importRecipeForm = useForm({
defaultValues: {
deeplink: '',
@@ -124,6 +148,14 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
recipe = await parseYamlFile(fileContent);
}
+ const titleValidationError = await validateTitleUniqueness(
+ value.recipeTitle.trim(),
+ value.global
+ );
+ if (titleValidationError) {
+ throw new Error(titleValidationError);
+ }
+
const validationResult = validateRecipe(recipe);
if (!validationResult.success) {
const errorMessages = getValidationErrorMessages(validationResult.errors);
From 9c29327e61558afa8cedd8aab286551b54b67e89 Mon Sep 17 00:00:00 2001
From: Amed Rodriguez
Date: Fri, 12 Sep 2025 11:50:45 -0700
Subject: [PATCH 4/7] update form schema validation for title requirements
---
ui/desktop/src/components/recipes/ImportRecipeForm.tsx | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
index fc250a4bbeaa..c81dd1546b2e 100644
--- a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
+++ b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
@@ -39,7 +39,15 @@ const importRecipeSchema = z
if (!file) return true;
return file.size <= 1024 * 1024;
}, 'File is too large, max size is 1MB'),
- recipeTitle: z.string().min(1, 'Recipe title is required'),
+ recipeTitle: z
+ .string()
+ .min(1, 'Recipe title is required')
+ .max(100, 'Recipe title must be 100 characters or less')
+ .refine((title) => title.trim().length > 0, 'Recipe title cannot be empty')
+ .refine(
+ (title) => /^[^<>:"/\\|?*]+$/.test(title.trim()),
+ 'Recipe title contains invalid characters (< > : " / \\ | ? *)'
+ ),
global: z.boolean(),
})
.refine((data) => (data.deeplink && data.deeplink.trim()) || data.yamlFile, {
From 5aff0b0c2d10b386bd5300c93e43e0bfb7db3b24 Mon Sep 17 00:00:00 2001
From: Amed Rodriguez
Date: Fri, 12 Sep 2025 14:37:16 -0700
Subject: [PATCH 5/7] set title onsubmit
---
ui/desktop/src/components/recipes/ImportRecipeForm.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
index c81dd1546b2e..96d30ff4f48e 100644
--- a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
+++ b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
@@ -156,6 +156,8 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
recipe = await parseYamlFile(fileContent);
}
+ recipe.title = value.recipeTitle.trim();
+
const titleValidationError = await validateTitleUniqueness(
value.recipeTitle.trim(),
value.global
From ab1c25fbd3e614e20b511d925b964637886cee61 Mon Sep 17 00:00:00 2001
From: Amed Rodriguez
Date: Fri, 12 Sep 2025 14:42:18 -0700
Subject: [PATCH 6/7] cosmetics
---
ui/desktop/src/components/recipes/ImportRecipeForm.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
index 96d30ff4f48e..ce2a563d377b 100644
--- a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
+++ b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
@@ -120,7 +120,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
);
if (titleExists) {
- return `A recipe with the title "${title}" already exists in ${isGlobal ? 'global' : 'directory'} recipes`;
+ return `A recipe with the same title already exists`;
}
} catch (error) {
console.warn('Failed to validate title uniqueness:', error);
@@ -515,7 +515,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
Expected Recipe Structure:
-
+
{JSON.stringify(getRecipeJsonSchema(), null, 2)}
From 9f253e5c33e38416de7546784571d4088d1b19de Mon Sep 17 00:00:00 2001
From: Amed Rodriguez
Date: Fri, 12 Sep 2025 16:00:48 -0700
Subject: [PATCH 7/7] add deprecation notice
---
ui/desktop/src/recipe/recipeStorage.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/ui/desktop/src/recipe/recipeStorage.ts b/ui/desktop/src/recipe/recipeStorage.ts
index 4778071888b7..c012c5dc9bd1 100644
--- a/ui/desktop/src/recipe/recipeStorage.ts
+++ b/ui/desktop/src/recipe/recipeStorage.ts
@@ -82,6 +82,8 @@ export async function saveRecipe(recipe: Recipe, options: SaveRecipeOptions): Pr
throw new Error('Invalid recipe title - cannot generate filename');
}
} else {
+ // This branch should now be considered deprecated and will be removed once the same functionality
+ // is incorporated in CreateRecipeForm
sanitizedName = sanitizeRecipeName(name);
if (!sanitizedName) {
throw new Error('Invalid recipe name');