diff --git a/src/core/config/CustomModesSchema.ts b/src/core/config/CustomModesSchema.ts index 29f5f3eac9..4cf85cbe45 100644 --- a/src/core/config/CustomModesSchema.ts +++ b/src/core/config/CustomModesSchema.ts @@ -29,22 +29,19 @@ const GroupOptionsSchema = z.object({ const GroupEntrySchema = z.union([ToolGroupSchema, z.tuple([ToolGroupSchema, GroupOptionsSchema])]) // Schema for array of groups -const GroupsArraySchema = z - .array(GroupEntrySchema) - .min(1, "At least one tool group is required") - .refine( - (groups) => { - const seen = new Set() - return groups.every((group) => { - // For tuples, check the group name (first element) - const groupName = Array.isArray(group) ? group[0] : group - if (seen.has(groupName)) return false - seen.add(groupName) - return true - }) - }, - { message: "Duplicate groups are not allowed" }, - ) +const GroupsArraySchema = z.array(GroupEntrySchema).refine( + (groups) => { + const seen = new Set() + return groups.every((group) => { + // For tuples, check the group name (first element) + const groupName = Array.isArray(group) ? group[0] : group + if (seen.has(groupName)) return false + seen.add(groupName) + return true + }) + }, + { message: "Duplicate groups are not allowed" }, +) // Schema for mode configuration export const CustomModeSchema = z.object({ diff --git a/src/core/config/__tests__/CustomModesSchema.test.ts b/src/core/config/__tests__/CustomModesSchema.test.ts index 9af3ba7110..f46bac2c7e 100644 --- a/src/core/config/__tests__/CustomModesSchema.test.ts +++ b/src/core/config/__tests__/CustomModesSchema.test.ts @@ -95,17 +95,6 @@ describe("CustomModeSchema", () => { expect(() => validateCustomMode(invalidGroupMode)).toThrow(ZodError) }) - test("rejects empty groups array", () => { - const invalidMode = { - slug: "123e4567-e89b-12d3-a456-426614174000", - name: "Test Mode", - roleDefinition: "Test role definition", - groups: [] as const, - } satisfies ModeConfig - - expect(() => validateCustomMode(invalidMode)).toThrow("At least one tool group is required") - }) - test("handles null and undefined gracefully", () => { expect(() => validateCustomMode(null)).toThrow(ZodError) expect(() => validateCustomMode(undefined)).toThrow(ZodError) @@ -179,16 +168,5 @@ describe("CustomModeSchema", () => { expect(() => CustomModeSchema.parse(modeWithDuplicates)).toThrow(/Duplicate groups/) }) - - it("requires at least one group", () => { - const modeWithNoGroups = { - slug: "test", - name: "Test", - roleDefinition: "Test", - groups: [], - } - - expect(() => CustomModeSchema.parse(modeWithNoGroups)).toThrow(/At least one tool group is required/) - }) }) }) diff --git a/src/core/config/__tests__/GroupConfigSchema.test.ts b/src/core/config/__tests__/GroupConfigSchema.test.ts index 3b1452b88d..68d0f6b69a 100644 --- a/src/core/config/__tests__/GroupConfigSchema.test.ts +++ b/src/core/config/__tests__/GroupConfigSchema.test.ts @@ -45,15 +45,6 @@ describe("GroupConfigSchema", () => { expect(() => CustomModeSchema.parse(mode)).toThrow() }) - test("rejects empty groups array", () => { - const mode = { - ...validBaseMode, - groups: [] as const, - } satisfies ModeConfig - - expect(() => CustomModeSchema.parse(mode)).toThrow("At least one tool group is required") - }) - test("rejects invalid group names", () => { const mode = { ...validBaseMode, diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index 5956ff6265..51472d4cab 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -19,6 +19,7 @@ import { ModeConfig, GroupEntry, } from "../../../../src/shared/modes" +import { CustomModeSchema } from "../../../../src/core/config/CustomModesSchema" import { supportPrompt, SupportPromptType, @@ -157,15 +158,34 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const [newModeGroups, setNewModeGroups] = useState(availableGroups) const [newModeSource, setNewModeSource] = useState("global") + // Field-specific error states + const [nameError, setNameError] = useState("") + const [slugError, setSlugError] = useState("") + const [roleDefinitionError, setRoleDefinitionError] = useState("") + const [groupsError, setGroupsError] = useState("") + + // Helper to reset form state + const resetFormState = useCallback(() => { + // Reset form fields + setNewModeName("") + setNewModeSlug("") + setNewModeGroups(availableGroups) + setNewModeRoleDefinition("") + setNewModeCustomInstructions("") + setNewModeSource("global") + // Reset error states + setNameError("") + setSlugError("") + setRoleDefinitionError("") + setGroupsError("") + }, []) + // Reset form fields when dialog opens useEffect(() => { if (isCreateModeDialogOpen) { - setNewModeGroups(availableGroups) - setNewModeRoleDefinition("") - setNewModeCustomInstructions("") - setNewModeSource("global") + resetFormState() } - }, [isCreateModeDialogOpen]) + }, [isCreateModeDialogOpen, resetFormState]) // Helper function to generate a unique slug from a name const generateSlug = useCallback((name: string, attempt = 0): string => { @@ -186,26 +206,52 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { ) const handleCreateMode = useCallback(() => { - if (!newModeName.trim() || !newModeSlug.trim()) return + // Clear previous errors + setNameError("") + setSlugError("") + setRoleDefinitionError("") + setGroupsError("") const source = newModeSource const newMode: ModeConfig = { slug: newModeSlug, name: newModeName, - roleDefinition: newModeRoleDefinition.trim() || "", + roleDefinition: newModeRoleDefinition.trim(), customInstructions: newModeCustomInstructions.trim() || undefined, groups: newModeGroups, source, } + + // Validate the mode against the schema + const result = CustomModeSchema.safeParse(newMode) + if (!result.success) { + // Map Zod errors to specific fields + result.error.errors.forEach((error) => { + const field = error.path[0] as string + const message = error.message + + switch (field) { + case "name": + setNameError(message) + break + case "slug": + setSlugError(message) + break + case "roleDefinition": + setRoleDefinitionError(message) + break + case "groups": + setGroupsError(message) + break + } + }) + return + } + updateCustomMode(newModeSlug, newMode) switchMode(newModeSlug) setIsCreateModeDialogOpen(false) - setNewModeName("") - setNewModeSlug("") - setNewModeRoleDefinition("") - setNewModeCustomInstructions("") - setNewModeGroups(availableGroups) - setNewModeSource("global") + resetFormState() // eslint-disable-next-line react-hooks/exhaustive-deps }, [ newModeName, @@ -431,7 +477,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
e.stopPropagation()} className="flex justify-between items-center mb-3"> -

Mode-Specific Prompts

+

Modes

@@ -727,7 +773,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { alignItems: "center", marginBottom: "4px", }}> -
Mode-specific Custom Instructions
+
Mode-specific Custom Instructions (optional)
{!findModeBySlug(mode, customModes) && ( { }} style={{ width: "100%" }} /> + {nameError && ( +
{nameError}
+ )}
Slug
@@ -1091,6 +1140,9 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { The slug is used in URLs and file names. It should be lowercase and contain only letters, numbers, and hyphens.
+ {slugError && ( +
{slugError}
+ )}
Save Location
@@ -1147,6 +1199,11 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { resize="vertical" style={{ width: "100%" }} /> + {roleDefinitionError && ( +
+ {roleDefinitionError} +
+ )}
Available Tools
@@ -1184,9 +1241,14 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { ))}
+ {groupsError && ( +
{groupsError}
+ )}
-
Custom Instructions
+
+ Custom Instructions (optional) +
{ backgroundColor: "var(--vscode-editor-background)", }}> setIsCreateModeDialogOpen(false)}>Cancel - + Create Mode
diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index 7c83662c5f..4e36246063 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -84,6 +84,7 @@ --color-vscode-notifications-background: var(--vscode-notifications-background); --color-vscode-notifications-border: var(--vscode-notifications-border); --color-vscode-descriptionForeground: var(--vscode-descriptionForeground); + --color-vscode-errorForeground: var(--vscode-errorForeground); } @layer base {