import setWith from 'lodash-es/setWith.js'; import * as React from 'react'; import { Box, Text, useTheme } from '@anthropic/ink'; import type { ValidationError } from '../utils/settings/validation.js'; import { type TreeNode, treeify } from '../utils/treeify.js'; /** * Builds a nested tree structure from dot-notation paths * Uses lodash setWith to avoid automatic array creation */ function buildNestedTree(errors: ValidationError[]): TreeNode { const tree: TreeNode = {}; errors.forEach(error => { if (!error.path) { // Root level error - use empty string as key tree[''] = error.message; return; } // Try to enhance the path with meaningful values const pathParts = error.path.split('.'); let modifiedPath = error.path; // If we have an invalid value, try to make the path more readable if (error.invalidValue !== null && error.invalidValue !== undefined && pathParts.length > 0) { const newPathParts: string[] = []; for (let i = 0; i < pathParts.length; i++) { const part = pathParts[i]; if (!part) continue; const numericPart = parseInt(part, 10); // If this is a numeric index and it's the last part where we have the invalid value if (!isNaN(numericPart) && i === pathParts.length - 1) { // Format the value for display let displayValue: string; if (typeof error.invalidValue === 'string') { displayValue = `"${error.invalidValue}"`; } else if (error.invalidValue === null) { displayValue = 'null'; } else if (error.invalidValue === undefined) { displayValue = 'undefined'; } else { displayValue = String(error.invalidValue); } newPathParts.push(displayValue); } else { // Keep other parts as-is newPathParts.push(part); } } modifiedPath = newPathParts.join('.'); } setWith(tree, modifiedPath, error.message, Object); }); return tree; } /** * Groups and displays validation errors using treeify with deduplication */ export function ValidationErrorsList({ errors }: { errors: ValidationError[] }): React.ReactNode { const [themeName] = useTheme(); if (errors.length === 0) { return null; } // Group errors by file const errorsByFile = errors.reduce>((acc, error) => { const file = error.file || '(file not specified)'; if (!acc[file]) { acc[file] = []; } acc[file]!.push(error); return acc; }, {}); // Sort files alphabetically const sortedFiles = Object.keys(errorsByFile).sort(); return ( {sortedFiles.map(file => { const fileErrors = errorsByFile[file] || []; // Sort errors by path fileErrors.sort((a, b) => { if (!a.path && b.path) return -1; if (a.path && !b.path) return 1; return (a.path || '').localeCompare(b.path || ''); }); // Build nested tree structure from error paths const errorTree = buildNestedTree(fileErrors); // Collect unique suggestion+docLink pairs const suggestionPairs = new Map(); fileErrors.forEach(error => { if (error.suggestion || error.docLink) { // Create a key from suggestion+docLink combination const key = `${error.suggestion || ''}|${error.docLink || ''}`; if (!suggestionPairs.has(key)) { suggestionPairs.set(key, { suggestion: error.suggestion, docLink: error.docLink, }); } } }); // Render the tree const treeOutput = treeify(errorTree, { showValues: true, themeName, treeCharColors: { treeChar: 'inactive', key: 'text', value: 'inactive', }, }); return ( {file} {treeOutput} {/* Display unique suggestion+docLink pairs */} {suggestionPairs.size > 0 && ( {Array.from(suggestionPairs.values()).map((pair, index) => ( {pair.suggestion && ( {pair.suggestion} )} {pair.docLink && ( Learn more: {pair.docLink} )} ))} )} ); })} ); }