mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Validate custom modes schema before creation from the UI
This commit is contained in:
parent
998beeed52
commit
7bbf097fa8
5 changed files with 93 additions and 67 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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/)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<GroupEntry[]>(availableGroups)
|
||||
const [newModeSource, setNewModeSource] = useState<ModeSource>("global")
|
||||
|
||||
// Field-specific error states
|
||||
const [nameError, setNameError] = useState<string>("")
|
||||
const [slugError, setSlugError] = useState<string>("")
|
||||
const [roleDefinitionError, setRoleDefinitionError] = useState<string>("")
|
||||
const [groupsError, setGroupsError] = useState<string>("")
|
||||
|
||||
// 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) => {
|
|||
|
||||
<div className="mt-5">
|
||||
<div onClick={(e) => e.stopPropagation()} className="flex justify-between items-center mb-3">
|
||||
<h3 className="text-vscode-foreground m-0">Mode-Specific Prompts</h3>
|
||||
<h3 className="text-vscode-foreground m-0">Modes</h3>
|
||||
<div className="flex gap-2">
|
||||
<VSCodeButton appearance="icon" onClick={openCreateModeDialog} title="Create new mode">
|
||||
<span className="codicon codicon-add"></span>
|
||||
|
|
@ -727,7 +773,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
alignItems: "center",
|
||||
marginBottom: "4px",
|
||||
}}>
|
||||
<div style={{ fontWeight: "bold" }}>Mode-specific Custom Instructions</div>
|
||||
<div style={{ fontWeight: "bold" }}>Mode-specific Custom Instructions (optional)</div>
|
||||
{!findModeBySlug(mode, customModes) && (
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
|
|
@ -1069,6 +1115,9 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
}}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
{nameError && (
|
||||
<div className="text-xs text-vscode-errorForeground mt-1">{nameError}</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Slug</div>
|
||||
|
|
@ -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.
|
||||
</div>
|
||||
{slugError && (
|
||||
<div className="text-xs text-vscode-errorForeground mt-1">{slugError}</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Save Location</div>
|
||||
|
|
@ -1147,6 +1199,11 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
resize="vertical"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
{roleDefinitionError && (
|
||||
<div className="text-xs text-vscode-errorForeground mt-1">
|
||||
{roleDefinitionError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Available Tools</div>
|
||||
|
|
@ -1184,9 +1241,14 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
</VSCodeCheckbox>
|
||||
))}
|
||||
</div>
|
||||
{groupsError && (
|
||||
<div className="text-xs text-vscode-errorForeground mt-1">{groupsError}</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Custom Instructions</div>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>
|
||||
Custom Instructions (optional)
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
|
|
@ -1219,10 +1281,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
backgroundColor: "var(--vscode-editor-background)",
|
||||
}}>
|
||||
<VSCodeButton onClick={() => setIsCreateModeDialogOpen(false)}>Cancel</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
onClick={handleCreateMode}
|
||||
disabled={!newModeName.trim() || !newModeSlug.trim()}>
|
||||
<VSCodeButton appearance="primary" onClick={handleCreateMode}>
|
||||
Create Mode
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue