mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
feat: implement comprehensive field validation for custom modes
- Enhanced CustomModesManager.ts with comprehensive validation for all fields - Added validation for required fields (name, roleDefinition, slug) and optional fields (description, whenToUse, customInstructions) - Implemented checks to prevent empty strings from being saved (should be undefined instead) - Extended local state management pattern from PR #5794 to ALL editable fields in ModesView.tsx - Added local state variables for all fields: localModeName, localModeDescription, localModeRoleDefinition, localModeWhenToUse, localModeCustomInstructions - Implemented onFocus/onChange/onBlur pattern for all editable fields to prevent empty field saves - Users can now visually clear fields during editing but empty values are prevented from being saved - This prevents custom modes from disappearing due to empty field validation issues Fixes custom modes disappearing and creation issues by extending validation pattern from PR #5794
This commit is contained in:
parent
acb360b9de
commit
45df949a17
2 changed files with 188 additions and 22 deletions
|
|
@ -470,11 +470,52 @@ export class CustomModesManager {
|
|||
|
||||
public async updateCustomMode(slug: string, config: ModeConfig): Promise<void> {
|
||||
try {
|
||||
// Validate the mode configuration before saving
|
||||
// Comprehensive validation to prevent empty fields from being saved
|
||||
const validationErrors: string[] = []
|
||||
|
||||
// Validate required fields are not empty
|
||||
if (!config.name || config.name.trim() === "") {
|
||||
validationErrors.push("Mode name cannot be empty")
|
||||
}
|
||||
|
||||
if (!config.roleDefinition || config.roleDefinition.trim() === "") {
|
||||
validationErrors.push("Role definition cannot be empty")
|
||||
}
|
||||
|
||||
if (!config.slug || config.slug.trim() === "") {
|
||||
validationErrors.push("Mode slug cannot be empty")
|
||||
}
|
||||
|
||||
// Validate optional fields are not empty strings (they should be undefined if not provided)
|
||||
if (config.description !== undefined && config.description.trim() === "") {
|
||||
validationErrors.push("Description cannot be empty (use undefined instead)")
|
||||
}
|
||||
|
||||
if (config.whenToUse !== undefined && config.whenToUse.trim() === "") {
|
||||
validationErrors.push("When to use cannot be empty (use undefined instead)")
|
||||
}
|
||||
|
||||
if (config.customInstructions !== undefined && config.customInstructions.trim() === "") {
|
||||
validationErrors.push("Custom instructions cannot be empty (use undefined instead)")
|
||||
}
|
||||
|
||||
// Validate groups array is not empty
|
||||
if (!config.groups || !Array.isArray(config.groups) || config.groups.length === 0) {
|
||||
validationErrors.push("At least one tool group must be selected")
|
||||
}
|
||||
|
||||
// If we have validation errors, throw them
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessage = `Invalid mode configuration: ${validationErrors.join(", ")}`
|
||||
logger.error(`Validation failed for mode ${slug}`, { errors: validationErrors })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// Use schema validation as a secondary check
|
||||
const validationResult = modeConfigSchema.safeParse(config)
|
||||
if (!validationResult.success) {
|
||||
const errors = validationResult.error.errors.map((e: any) => e.message).join(", ")
|
||||
logger.error(`Invalid mode configuration for ${slug}`, { errors: validationResult.error.errors })
|
||||
logger.error(`Schema validation failed for mode ${slug}`, { errors: validationResult.error.errors })
|
||||
throw new Error(`Invalid mode configuration: ${errors}`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,9 +110,14 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
const [searchValue, setSearchValue] = useState("")
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Local state for mode name input to allow visual emptying
|
||||
// Local state for all editable fields to allow visual emptying but prevent saving empty values
|
||||
const [localModeName, setLocalModeName] = useState<string>("")
|
||||
const [localModeDescription, setLocalModeDescription] = useState<string>("")
|
||||
const [localModeRoleDefinition, setLocalModeRoleDefinition] = useState<string>("")
|
||||
const [localModeWhenToUse, setLocalModeWhenToUse] = useState<string>("")
|
||||
const [localModeCustomInstructions, setLocalModeCustomInstructions] = useState<string>("")
|
||||
const [currentEditingModeSlug, setCurrentEditingModeSlug] = useState<string | null>(null)
|
||||
const [currentEditingField, setCurrentEditingField] = useState<string | null>(null)
|
||||
|
||||
// Direct update functions
|
||||
const updateAgentPrompt = useCallback(
|
||||
|
|
@ -222,11 +227,16 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
}
|
||||
}, [getCurrentMode, checkRulesDirectory, hasRulesToExport])
|
||||
|
||||
// Reset local name state when mode changes
|
||||
// Reset all local state when mode changes
|
||||
useEffect(() => {
|
||||
if (currentEditingModeSlug && currentEditingModeSlug !== visualMode) {
|
||||
setCurrentEditingModeSlug(null)
|
||||
setCurrentEditingField(null)
|
||||
setLocalModeName("")
|
||||
setLocalModeDescription("")
|
||||
setLocalModeRoleDefinition("")
|
||||
setLocalModeWhenToUse("")
|
||||
setLocalModeCustomInstructions("")
|
||||
}
|
||||
}, [visualMode, currentEditingModeSlug])
|
||||
|
||||
|
|
@ -825,30 +835,58 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
value={(() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
const prompt = customModePrompts?.[visualMode] as PromptComponent
|
||||
|
||||
// Use local state if currently editing this field
|
||||
if (currentEditingField === "roleDefinition" && currentEditingModeSlug === visualMode) {
|
||||
return localModeRoleDefinition
|
||||
}
|
||||
|
||||
return (
|
||||
customMode?.roleDefinition ??
|
||||
prompt?.roleDefinition ??
|
||||
getRoleDefinition(visualMode)
|
||||
)
|
||||
})()}
|
||||
onFocus={() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
const prompt = customModePrompts?.[visualMode] as PromptComponent
|
||||
const currentValue = customMode?.roleDefinition ??
|
||||
prompt?.roleDefinition ??
|
||||
getRoleDefinition(visualMode)
|
||||
|
||||
setCurrentEditingModeSlug(visualMode)
|
||||
setCurrentEditingField("roleDefinition")
|
||||
setLocalModeRoleDefinition(currentValue)
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const value =
|
||||
(e as unknown as CustomEvent)?.detail?.target?.value ||
|
||||
((e as any).target as HTMLTextAreaElement).value
|
||||
setLocalModeRoleDefinition(value)
|
||||
}}
|
||||
onBlur={() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
if (customMode) {
|
||||
// For custom modes, update the JSON file
|
||||
updateCustomMode(visualMode, {
|
||||
...customMode,
|
||||
roleDefinition: value.trim() || "",
|
||||
source: customMode.source || "global",
|
||||
})
|
||||
} else {
|
||||
// For built-in modes, update the prompts
|
||||
updateAgentPrompt(visualMode, {
|
||||
roleDefinition: value.trim() || undefined,
|
||||
})
|
||||
|
||||
// Only save if the value is not empty
|
||||
if (localModeRoleDefinition.trim()) {
|
||||
if (customMode) {
|
||||
// For custom modes, update the JSON file
|
||||
updateCustomMode(visualMode, {
|
||||
...customMode,
|
||||
roleDefinition: localModeRoleDefinition.trim(),
|
||||
source: customMode.source || "global",
|
||||
})
|
||||
} else {
|
||||
// For built-in modes, update the prompts
|
||||
updateAgentPrompt(visualMode, {
|
||||
roleDefinition: localModeRoleDefinition.trim(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the editing state
|
||||
setCurrentEditingField(null)
|
||||
setCurrentEditingModeSlug(null)
|
||||
}}
|
||||
className="w-full"
|
||||
rows={5}
|
||||
|
|
@ -884,26 +922,55 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
value={(() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
const prompt = customModePrompts?.[visualMode] as PromptComponent
|
||||
|
||||
// Use local state if currently editing this field
|
||||
if (currentEditingField === "description" && currentEditingModeSlug === visualMode) {
|
||||
return localModeDescription
|
||||
}
|
||||
|
||||
return customMode?.description ?? prompt?.description ?? getDescription(visualMode)
|
||||
})()}
|
||||
onFocus={() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
const prompt = customModePrompts?.[visualMode] as PromptComponent
|
||||
const currentValue = customMode?.description ??
|
||||
prompt?.description ??
|
||||
getDescription(visualMode)
|
||||
|
||||
setCurrentEditingModeSlug(visualMode)
|
||||
setCurrentEditingField("description")
|
||||
setLocalModeDescription(currentValue || "")
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const value =
|
||||
(e as unknown as CustomEvent)?.detail?.target?.value ||
|
||||
((e as any).target as HTMLTextAreaElement).value
|
||||
setLocalModeDescription(value)
|
||||
}}
|
||||
onBlur={() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
|
||||
// For description, allow empty values (they become undefined)
|
||||
const trimmedValue = localModeDescription.trim()
|
||||
const finalValue = trimmedValue || undefined
|
||||
|
||||
if (customMode) {
|
||||
// For custom modes, update the JSON file
|
||||
updateCustomMode(visualMode, {
|
||||
...customMode,
|
||||
description: value.trim() || undefined,
|
||||
description: finalValue,
|
||||
source: customMode.source || "global",
|
||||
})
|
||||
} else {
|
||||
// For built-in modes, update the prompts
|
||||
updateAgentPrompt(visualMode, {
|
||||
description: value.trim() || undefined,
|
||||
description: finalValue,
|
||||
})
|
||||
}
|
||||
|
||||
// Clear the editing state
|
||||
setCurrentEditingField(null)
|
||||
setCurrentEditingModeSlug(null)
|
||||
}}
|
||||
className="w-full"
|
||||
data-testid={`${getCurrentMode()?.slug || "code"}-description-textfield`}
|
||||
|
|
@ -939,26 +1006,55 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
value={(() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
const prompt = customModePrompts?.[visualMode] as PromptComponent
|
||||
|
||||
// Use local state if currently editing this field
|
||||
if (currentEditingField === "whenToUse" && currentEditingModeSlug === visualMode) {
|
||||
return localModeWhenToUse
|
||||
}
|
||||
|
||||
return customMode?.whenToUse ?? prompt?.whenToUse ?? getWhenToUse(visualMode)
|
||||
})()}
|
||||
onFocus={() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
const prompt = customModePrompts?.[visualMode] as PromptComponent
|
||||
const currentValue = customMode?.whenToUse ??
|
||||
prompt?.whenToUse ??
|
||||
getWhenToUse(visualMode)
|
||||
|
||||
setCurrentEditingModeSlug(visualMode)
|
||||
setCurrentEditingField("whenToUse")
|
||||
setLocalModeWhenToUse(currentValue || "")
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const value =
|
||||
(e as unknown as CustomEvent)?.detail?.target?.value ||
|
||||
((e as any).target as HTMLTextAreaElement).value
|
||||
setLocalModeWhenToUse(value)
|
||||
}}
|
||||
onBlur={() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
|
||||
// For whenToUse, allow empty values (they become undefined)
|
||||
const trimmedValue = localModeWhenToUse.trim()
|
||||
const finalValue = trimmedValue || undefined
|
||||
|
||||
if (customMode) {
|
||||
// For custom modes, update the JSON file
|
||||
updateCustomMode(visualMode, {
|
||||
...customMode,
|
||||
whenToUse: value.trim() || undefined,
|
||||
whenToUse: finalValue,
|
||||
source: customMode.source || "global",
|
||||
})
|
||||
} else {
|
||||
// For built-in modes, update the prompts
|
||||
updateAgentPrompt(visualMode, {
|
||||
whenToUse: value.trim() || undefined,
|
||||
whenToUse: finalValue,
|
||||
})
|
||||
}
|
||||
|
||||
// Clear the editing state
|
||||
setCurrentEditingField(null)
|
||||
setCurrentEditingModeSlug(null)
|
||||
}}
|
||||
className="w-full"
|
||||
rows={4}
|
||||
|
|
@ -1094,22 +1190,47 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
value={(() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
const prompt = customModePrompts?.[visualMode] as PromptComponent
|
||||
|
||||
// Use local state if currently editing this field
|
||||
if (currentEditingField === "customInstructions" && currentEditingModeSlug === visualMode) {
|
||||
return localModeCustomInstructions
|
||||
}
|
||||
|
||||
return (
|
||||
customMode?.customInstructions ??
|
||||
prompt?.customInstructions ??
|
||||
getCustomInstructions(mode, customModes)
|
||||
)
|
||||
})()}
|
||||
onFocus={() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
const prompt = customModePrompts?.[visualMode] as PromptComponent
|
||||
const currentValue = customMode?.customInstructions ??
|
||||
prompt?.customInstructions ??
|
||||
getCustomInstructions(mode, customModes)
|
||||
|
||||
setCurrentEditingModeSlug(visualMode)
|
||||
setCurrentEditingField("customInstructions")
|
||||
setLocalModeCustomInstructions(currentValue || "")
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const value =
|
||||
(e as unknown as CustomEvent)?.detail?.target?.value ||
|
||||
((e as any).target as HTMLTextAreaElement).value
|
||||
setLocalModeCustomInstructions(value)
|
||||
}}
|
||||
onBlur={() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
|
||||
// For customInstructions, allow empty values (they become undefined)
|
||||
const trimmedValue = localModeCustomInstructions.trim()
|
||||
const finalValue = trimmedValue || undefined
|
||||
|
||||
if (customMode) {
|
||||
// For custom modes, update the JSON file
|
||||
updateCustomMode(visualMode, {
|
||||
...customMode,
|
||||
customInstructions: value.trim() || undefined,
|
||||
customInstructions: finalValue,
|
||||
source: customMode.source || "global",
|
||||
})
|
||||
} else {
|
||||
|
|
@ -1117,9 +1238,13 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
const existingPrompt = customModePrompts?.[visualMode] as PromptComponent
|
||||
updateAgentPrompt(visualMode, {
|
||||
...existingPrompt,
|
||||
customInstructions: value.trim(),
|
||||
customInstructions: finalValue,
|
||||
})
|
||||
}
|
||||
|
||||
// Clear the editing state
|
||||
setCurrentEditingField(null)
|
||||
setCurrentEditingModeSlug(null)
|
||||
}}
|
||||
rows={10}
|
||||
className="w-full"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue