mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
feat: add ESLint with TypeScript/React plugins and fix all lint errors
- Add ESLint 9 configuration (eslint.config.mjs) with TypeScript and React hooks plugins
- Add npm run lint:eslint command for code quality checks
- Expand npm run lint to check all three TypeScript configs (renderer, main, cli)
- Update tsconfig.cli.json to include src/prompts and src/types directories
Fix 29 ESLint errors:
- Remove unused updateCliActivity import in batch-processor.ts
- Convert {false && <jsx>} patterns to comments in AutoRun components
- Wrap case block with const declarations in braces (AgentSelectionScreen)
- Fix unused expression pattern in PreparingPlanScreen
- Fix conditional hook calls in FilePreview, OfflineQueueBanner, RecentCommandChips, TerminalOutput
- Add windows-diagnostics.json to PackageContents interface
Update CLAUDE.md and CONTRIBUTING.md with new linting commands and documentation.
Claude ID: 029e8abe-5734-4967-9fb4-c85078c1973d
Maestro ID: 87ffa06e-0ecd-4eb8-b327-dad1ec24f7a9
This commit is contained in:
@@ -31,7 +31,8 @@ npm run dev # Development with hot reload
|
||||
npm run dev:web # Web interface development
|
||||
npm run build # Full production build
|
||||
npm run clean # Clean build artifacts
|
||||
npm run lint # TypeScript type checking
|
||||
npm run lint # TypeScript type checking (all configs)
|
||||
npm run lint:eslint # ESLint code quality checks
|
||||
npm run package # Package for all platforms
|
||||
npm run test # Run test suite
|
||||
npm run test:watch # Run tests in watch mode
|
||||
|
||||
@@ -158,13 +158,27 @@ src/__tests__/
|
||||
|
||||
## Linting
|
||||
|
||||
Run TypeScript type checking to catch errors before building:
|
||||
Run TypeScript type checking and ESLint to catch errors before building:
|
||||
|
||||
```bash
|
||||
npm run lint # Run type checking across the codebase
|
||||
npm run lint # TypeScript type checking (all configs: renderer, main, cli)
|
||||
npm run lint:eslint # ESLint code quality checks (React hooks, unused vars, etc.)
|
||||
```
|
||||
|
||||
The linter uses a dedicated `tsconfig.lint.json` configuration that type-checks all source files without emitting output. This catches type errors, unused variables, and other TypeScript issues.
|
||||
### TypeScript Linting
|
||||
|
||||
The TypeScript linter checks all three build configurations:
|
||||
- `tsconfig.lint.json` - Renderer, web, and shared code
|
||||
- `tsconfig.main.json` - Main process code
|
||||
- `tsconfig.cli.json` - CLI tooling
|
||||
|
||||
### ESLint
|
||||
|
||||
ESLint is configured with TypeScript and React plugins (`eslint.config.mjs`):
|
||||
- `react-hooks/rules-of-hooks` - Enforces React hooks rules
|
||||
- `react-hooks/exhaustive-deps` - Warns about missing hook dependencies
|
||||
- `@typescript-eslint/no-unused-vars` - Warns about unused variables
|
||||
- `prefer-const` - Suggests const for never-reassigned variables
|
||||
|
||||
**When to run linting:**
|
||||
- Before committing changes
|
||||
@@ -175,7 +189,8 @@ The linter uses a dedicated `tsconfig.lint.json` configuration that type-checks
|
||||
- Unused imports or variables
|
||||
- Type mismatches in function calls
|
||||
- Missing required properties on interfaces
|
||||
- Incorrect generic type parameters
|
||||
- React hooks called conditionally (must be called in same order every render)
|
||||
- Missing dependencies in useEffect/useCallback/useMemo
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
|
||||
86
eslint.config.mjs
Normal file
86
eslint.config.mjs
Normal file
@@ -0,0 +1,86 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import globals from 'globals';
|
||||
|
||||
export default tseslint.config(
|
||||
// Ignore patterns
|
||||
{
|
||||
ignores: [
|
||||
'dist/**',
|
||||
'release/**',
|
||||
'node_modules/**',
|
||||
'*.config.js',
|
||||
'*.config.mjs',
|
||||
'*.config.ts',
|
||||
'scripts/**',
|
||||
'src/__tests__/**',
|
||||
'src/web/utils/serviceWorker.ts', // Service worker has special globals
|
||||
'src/web/public/**', // Service worker and static assets
|
||||
],
|
||||
},
|
||||
|
||||
// Base ESLint recommended rules
|
||||
eslint.configs.recommended,
|
||||
|
||||
// TypeScript ESLint recommended rules
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
// Main configuration for all TypeScript files
|
||||
{
|
||||
files: ['src/**/*.ts', 'src/**/*.tsx'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...globals.es2020,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
react: reactPlugin,
|
||||
'react-hooks': reactHooksPlugin,
|
||||
},
|
||||
rules: {
|
||||
// TypeScript-specific rules
|
||||
'@typescript-eslint/no-unused-vars': ['warn', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
}],
|
||||
'@typescript-eslint/no-explicit-any': 'off', // Too many existing uses
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off', // Used in main process
|
||||
|
||||
// React rules
|
||||
'react/jsx-uses-react': 'error',
|
||||
'react/jsx-uses-vars': 'error',
|
||||
'react/prop-types': 'off', // Using TypeScript for prop types
|
||||
'react/react-in-jsx-scope': 'off', // Not needed with new JSX transform
|
||||
|
||||
// React Hooks rules
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
|
||||
// General rules
|
||||
'no-console': 'off', // Console is used throughout
|
||||
'no-undef': 'off', // TypeScript handles this
|
||||
'no-control-regex': 'off', // Intentionally used for terminal escape sequences
|
||||
'no-useless-escape': 'off', // Sometimes needed for clarity in regexes
|
||||
'prefer-const': 'warn',
|
||||
'no-var': 'error',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
2824
package-lock.json
generated
2824
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -33,7 +33,8 @@
|
||||
"start": "electron .",
|
||||
"clean": "rm -rf dist release node_modules/.vite",
|
||||
"postinstall": "electron-rebuild -f -w node-pty",
|
||||
"lint": "tsc -p tsconfig.lint.json",
|
||||
"lint": "tsc -p tsconfig.lint.json && tsc -p tsconfig.main.json --noEmit && tsc -p tsconfig.cli.json --noEmit",
|
||||
"lint:eslint": "eslint src/",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
@@ -222,6 +223,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/notarize": "^3.1.1",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -234,6 +236,8 @@
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@vitest/coverage-v8": "^4.0.15",
|
||||
"autoprefixer": "^10.4.16",
|
||||
@@ -244,6 +248,10 @@
|
||||
"electron-playwright-helpers": "^2.0.1",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"esbuild": "^0.24.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"globals": "^16.5.0",
|
||||
"jsdom": "^27.2.0",
|
||||
"lucide-react": "^0.303.0",
|
||||
"playwright": "^1.57.0",
|
||||
@@ -252,6 +260,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-eslint": "^8.50.1",
|
||||
"vite": "^5.0.11",
|
||||
"vite-plugin-electron": "^0.28.2",
|
||||
"vitest": "^4.0.15"
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from './agent-spawner';
|
||||
import { addHistoryEntry, readGroups } from './storage';
|
||||
import { substituteTemplateVariables, TemplateContext } from '../../shared/templateVariables';
|
||||
import { registerCliActivity, updateCliActivity, unregisterCliActivity } from '../../shared/cli-activity';
|
||||
import { registerCliActivity, unregisterCliActivity } from '../../shared/cli-activity';
|
||||
import { logger } from '../../main/utils/logger';
|
||||
import { autorunSynopsisPrompt } from '../../prompts';
|
||||
import { parseSynopsis } from '../../shared/synopsis';
|
||||
|
||||
@@ -23,7 +23,6 @@ export interface PackageContents {
|
||||
'storage-info.json': unknown;
|
||||
'group-chats.json': unknown;
|
||||
'batch-state.json': unknown;
|
||||
'windows-diagnostics.json': unknown;
|
||||
'collection-errors.json'?: unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -1209,8 +1209,7 @@ const AutoRunInner = forwardRef<AutoRunHandle, AutoRunProps>(function AutoRunInn
|
||||
<Maximize2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{/* Image upload button - hidden for now, can be re-enabled by removing false && */}
|
||||
{false && (
|
||||
{/* Image upload button - hidden for now, can be re-enabled when needed
|
||||
<button
|
||||
onClick={() => mode === 'edit' && !isLocked && fileInputRef.current?.click()}
|
||||
disabled={mode !== 'edit' || isLocked}
|
||||
@@ -1226,7 +1225,7 @@ const AutoRunInner = forwardRef<AutoRunHandle, AutoRunProps>(function AutoRunInn
|
||||
>
|
||||
<Image className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
*/}
|
||||
<button
|
||||
onClick={() => !isLocked && switchMode('edit')}
|
||||
disabled={isLocked}
|
||||
|
||||
@@ -239,8 +239,7 @@ export function AutoRunExpandedModal({
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
Preview
|
||||
</button>
|
||||
{/* Image upload button - hidden for now, can be re-enabled by removing false && */}
|
||||
{false && (
|
||||
{/* Image upload button - hidden for now, can be re-enabled when needed
|
||||
<button
|
||||
onClick={() => localMode === 'edit' && !isLocked && fileInputRef.current?.click()}
|
||||
disabled={localMode !== 'edit' || isLocked}
|
||||
@@ -256,7 +255,7 @@ export function AutoRunExpandedModal({
|
||||
>
|
||||
<Image className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
*/}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
|
||||
@@ -427,6 +427,7 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
||||
const [editContent, setEditContent] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [showUnsavedChangesModal, setShowUnsavedChangesModal] = useState(false);
|
||||
const [copyNotificationMessage, setCopyNotificationMessage] = useState('');
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const codeContainerRef = useRef<HTMLDivElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -442,17 +443,18 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
||||
|
||||
const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack();
|
||||
|
||||
if (!file) return null;
|
||||
|
||||
const language = getLanguageFromFilename(file.name);
|
||||
// Compute derived values - must be before any early returns but after hooks
|
||||
const language = file ? getLanguageFromFilename(file.name) : '';
|
||||
const isMarkdown = language === 'markdown';
|
||||
const isImage = isImageFile(file.name);
|
||||
const isImage = file ? isImageFile(file.name) : false;
|
||||
|
||||
// Check for binary files - either by extension or by content analysis
|
||||
// Memoize to avoid recalculating on every render (content analysis can be expensive)
|
||||
const isBinary = useMemo(() => {
|
||||
if (!file) return false;
|
||||
if (isImage) return false;
|
||||
return isBinaryExtension(file.name) || isBinaryContent(file.content);
|
||||
}, [isImage, file.name, file.content]);
|
||||
}, [isImage, file]);
|
||||
|
||||
// Calculate task counts for markdown files
|
||||
const taskCounts = useMemo(() => {
|
||||
@@ -464,7 +466,7 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
||||
}, [isMarkdown, file?.content]);
|
||||
|
||||
// Extract directory path without filename
|
||||
const directoryPath = file.path.substring(0, file.path.lastIndexOf('/'));
|
||||
const directoryPath = file ? file.path.substring(0, file.path.lastIndexOf('/')) : '';
|
||||
|
||||
// Fetch file stats when file changes
|
||||
useEffect(() => {
|
||||
@@ -589,7 +591,7 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
||||
// Auto-focus on mount and when file changes so keyboard shortcuts work immediately
|
||||
useEffect(() => {
|
||||
containerRef.current?.focus();
|
||||
}, [file.path]); // Run on mount and when navigating to a different file
|
||||
}, [file?.path]); // Run on mount and when navigating to a different file
|
||||
|
||||
// Helper to handle escape key - shows confirmation modal if there are unsaved changes
|
||||
const handleEscapeRequest = useCallback(() => {
|
||||
@@ -726,7 +728,7 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
||||
});
|
||||
matchElementsRef.current = [];
|
||||
};
|
||||
}, [searchQuery, file.content, isMarkdown, isImage, theme.colors.accent]);
|
||||
}, [searchQuery, file?.content, isMarkdown, isImage, theme.colors.accent]);
|
||||
|
||||
// Search matches in markdown preview mode - use CSS Custom Highlight API
|
||||
useEffect(() => {
|
||||
@@ -808,7 +810,7 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
||||
};
|
||||
} else {
|
||||
// Fallback: count matches and scroll to location (no highlighting)
|
||||
const matches = file.content.match(searchRegex);
|
||||
const matches = file?.content?.match(searchRegex);
|
||||
const count = matches ? matches.length : 0;
|
||||
setTotalMatches(count);
|
||||
|
||||
@@ -838,11 +840,10 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
||||
}
|
||||
|
||||
matchElementsRef.current = [];
|
||||
}, [searchQuery, file.content, isMarkdown, markdownEditMode, currentMatchIndex, theme.colors.accent]);
|
||||
|
||||
const [copyNotificationMessage, setCopyNotificationMessage] = useState('');
|
||||
}, [searchQuery, file?.content, isMarkdown, markdownEditMode, currentMatchIndex, theme.colors.accent]);
|
||||
|
||||
const copyPathToClipboard = () => {
|
||||
if (!file) return;
|
||||
navigator.clipboard.writeText(file.path);
|
||||
setCopyNotificationMessage('File Path Copied to Clipboard');
|
||||
setShowCopyNotification(true);
|
||||
@@ -850,6 +851,7 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
||||
};
|
||||
|
||||
const copyContentToClipboard = async () => {
|
||||
if (!file) return;
|
||||
if (isImage) {
|
||||
// For images, copy the image to clipboard
|
||||
try {
|
||||
@@ -1084,6 +1086,9 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
||||
}
|
||||
};
|
||||
|
||||
// Early return if no file - must be after all hooks
|
||||
if (!file) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
|
||||
@@ -800,7 +800,8 @@ export const TerminalOutput = forwardRef<HTMLDivElement, TerminalOutputProps>((p
|
||||
} = props;
|
||||
|
||||
// Use the forwarded ref if provided, otherwise create a local one
|
||||
const terminalOutputRef = (ref as React.RefObject<HTMLDivElement>) || useRef<HTMLDivElement>(null);
|
||||
const localRef = useRef<HTMLDivElement>(null);
|
||||
const terminalOutputRef = (ref as React.RefObject<HTMLDivElement>) || localRef;
|
||||
|
||||
// Scroll container ref for native scrolling
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -495,7 +495,7 @@ export function AgentSelectionScreen({ theme }: AgentSelectionScreenProps): JSX.
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
case ' ': {
|
||||
e.preventDefault();
|
||||
// Select the focused tile if supported and detected
|
||||
const tile = AGENT_TILES[currentIndex];
|
||||
@@ -508,6 +508,7 @@ export function AgentSelectionScreen({ theme }: AgentSelectionScreenProps): JSX.
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isNameFieldFocused,
|
||||
|
||||
@@ -112,8 +112,9 @@ function FactContent({
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
// Open in system browser
|
||||
window.maestro?.shell?.openExternal?.(segment.url) ||
|
||||
if (!window.maestro?.shell?.openExternal?.(segment.url)) {
|
||||
window.open(segment.url, '_blank');
|
||||
}
|
||||
}}
|
||||
className="underline hover:opacity-80 cursor-pointer transition-opacity"
|
||||
style={{ color: theme.colors.accent }}
|
||||
|
||||
@@ -48,11 +48,6 @@ export function OfflineQueueBanner({
|
||||
const colors = useThemeColors();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// Don't show if queue is empty
|
||||
if (queue.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isProcessing = status === 'processing';
|
||||
const canRetry = !isOffline && isConnected && status !== 'processing';
|
||||
|
||||
@@ -78,6 +73,11 @@ export function OfflineQueueBanner({
|
||||
onRemoveCommand(commandId);
|
||||
}, [onRemoveCommand]);
|
||||
|
||||
// Don't show if queue is empty
|
||||
if (queue.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -54,11 +54,6 @@ export function RecentCommandChips({
|
||||
// Get limited number of commands
|
||||
const displayCommands = commands.slice(0, maxChips);
|
||||
|
||||
// Don't render if no commands
|
||||
if (displayCommands.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle chip tap
|
||||
*/
|
||||
@@ -71,6 +66,11 @@ export function RecentCommandChips({
|
||||
[disabled, onSelectCommand]
|
||||
);
|
||||
|
||||
// Don't render if no commands
|
||||
if (displayCommands.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
"declaration": false,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/cli/**/*", "src/shared/**/*"],
|
||||
"include": ["src/cli/**/*", "src/shared/**/*", "src/prompts/**/*", "src/types/**/*"],
|
||||
"exclude": ["node_modules", "dist", "release"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user