* Done. The ESLint rule is now in place, banning optional props in all ui-new components.
**Summary:**
- Added the rule to `frontend/.eslintrc.cjs` targeting `TSPropertySignature[optional=true]`
- The rule applies to all files under `src/components/ui-new/**/*.{ts,tsx}`
**Impact Assessment - 100 violations across:**
| Category | Files | Violations |
|----------|-------|------------|
| **primitives/** | 11 files | ~40 violations |
| **views/** | 5 files | ~15 violations |
| **containers/** | 5 files | ~10 violations |
| **dialogs/** | 7 files | ~15 violations |
| **Root-level** | 5 files | ~15 violations |
| **types/** | 1 file | 5 violations |
The most affected files are:
- `primitives/Dropdown.tsx` (14 violations)
- `primitives/RepoCard.tsx` (12 violations)
- `NewDisplayConversationEntry.tsx` (11 violations)
- `primitives/CommandBar.tsx` (5 violations)
Would you like me to start refactoring these components to comply with the new rule?
* Done. The rule now only applies to `containers/`.
**Summary:**
- Removed the optional props ban from the general `ui-new/**` override
- Added a new override specifically for `src/components/ui-new/containers/**/*.{ts,tsx}`
**Impact reduced from 100 to 48 violations**, all now in container components:
| File | Violations |
|------|------------|
| SessionChatBoxContainer.tsx | 11 |
| DiffViewCardWithComments.tsx | 9 |
| SearchableDropdownContainer.tsx | 6 |
| FileTreeContainer.tsx | 3 |
| PreviewControlsContainer.tsx | 3 |
| ChangesPanelContainer.tsx | 2 |
| LogsContentContainer.tsx | 2 |
| PreviewBrowserContainer.tsx | 2 |
| WorkspacesLayout.tsx | 2 |
| WorkspacesMainContainer.tsx | 2 |
| + 6 more files | 6 |
* All ESLint issues in `SessionChatBoxContainer.tsx` have been fixed. Here's a summary of the changes:
1. **Removed dead code**:
- Removed `taskId` prop (never passed by call site)
- Removed `workspaceId` aliasing and fallback (`propWorkspaceId ?? session?.workspace_id`)
2. **Updated interface** - Changed all optional props (`?`) to explicit types:
- Props that can be undefined use `Type | undefined` instead of `Type?`
- Props with defaults (`sessions`, `isNewSessionMode`) are now required in the interface
3. **Added explicit defaults** in destructuring for all props:
- `session = undefined`, `onSelectSession = undefined`, `projectId = undefined`, `onStartNewSession = undefined`, `workspaceId = undefined`
- `filesChanged = 0`, `linesAdded = 0`, `linesRemoved = 0`
- `sessions = []`, `isNewSessionMode = false`
* Done. I've refactored `SessionChatBoxContainer.tsx` to use a discriminated union for props. Here's a summary:
**Changes made:**
1. **Defined discriminated union type** with three modes:
- `existing-session`: Has a `session` (required), `onSelectSession`, `onStartNewSession`
- `new-session`: Has a `workspaceId` (required), `onSelectSession`
- `placeholder`: Has `workspaceId` (optional), no callbacks
2. **Updated component** to extract mode-specific values from props based on the discriminant
3. **Updated call site** (`WorkspacesMain.tsx`) to pass the correct mode and props based on state
**Benefits:**
- No more redundant `= undefined` defaults for props that are required in their respective modes
- Type safety ensures each mode has exactly the props it needs
- Self-documenting code with explicit mode discriminant
- ESLint and TypeScript checks pass
* Cleanup script changes for workspace f4ec6f6e-a524-4760-889a-7ec2ebbce4c6
* Both lint and type check pass. I've removed `workspaceId` from `PlaceholderProps` since it's not needed when the editor is disabled in placeholder mode.
* All tasks are complete. Here's a summary of the changes made:
## Summary
I've refactored `DiffViewCardWithComments.tsx` and its related files to fix 11 ESLint errors (9 in DiffViewCardWithComments, 1 in CommentWidgetLine, 1 in ReviewCommentRenderer).
### Changes Made:
1. **`DiffViewCardWithComments.tsx`** - Converted to use discriminated unions:
- `DiffInput` type: Changed optional props (`oldPath?`, `changeKind?`, `hasLineNumbers?`) to explicit union types (`oldPath: string | undefined`)
- `DiffViewCardWithCommentsProps`: Split into two modes using a discriminated union:
- `CollapsibleProps` (mode: 'collapsible') - requires `expanded` and `onToggle`
- `StaticProps` (mode: 'static') - always expanded, no toggle
- Base props (`status`, `className`, `projectId`, `attemptId`) now use `T | undefined` instead of `T?`
2. **`CommentWidgetLine.tsx`** - Changed `projectId?: string` to `projectId: string | undefined`
3. **`ReviewCommentRenderer.tsx`** - Changed `projectId?: string` to `projectId: string | undefined`
4. **`ChangesPanel.tsx`** (callsite) - Updated to pass the new required props:
- Added `mode="collapsible"`
- Added `status={undefined}`
- Added `className=""`
### Verification:
- ✅ ESLint passes for all modified files
- ✅ TypeScript type check passes
* Cleanup script changes for workspace f4ec6f6e-a524-4760-889a-7ec2ebbce4c6
* Done. Here's a summary of all changes:
## Summary
Refactored `DiffViewCardWithComments.tsx` and its callsites to fix ESLint issues:
### Changes Made:
1. **`DiffViewCardWithComments.tsx`**:
- Removed unused `status` prop and `ToolStatusDot` rendering
- Made `projectId` and `attemptId` required strings (no longer `undefined`)
- Removed conditional check for `attemptId` on the Open in IDE button (always shown now)
2. **`ChangesPanel.tsx`**:
- Made `projectId` and `attemptId` required strings
- Removed `status={undefined}` from the component call
3. **`CommentWidgetLine.tsx`**:
- Changed `projectId: string | undefined` to `projectId: string`
4. **`ReviewCommentRenderer.tsx`**:
- Changed `projectId: string | undefined` to `projectId: string`
5. **`ChangesPanelContainer.tsx`**:
- Made `className` and `attemptId` required props
- Added guard: if `projectId` is unavailable, renders empty panel
6. **`WorkspacesLayout.tsx`**:
- Added guard: only renders `ChangesPanelContainer` when `selectedWorkspace?.id` exists
- Passes required `className=""` prop
### Verification:
- ✅ TypeScript type check passes
- ✅ ESLint passes for all modified files
* Done. Changed `changeKind` from `DiffChangeKind | undefined` to `DiffChangeKind` in the `DiffInput` type. Both TypeScript and ESLint pass.
* Done. I've successfully refactored `SearchableDropdownContainer.tsx` to fix the 6 ESLint errors:
**Changes made:**
1. **`SearchableDropdownContainer.tsx`** (interface):
- `selectedValue: string | null` (removed `?`)
- `filterItem: ((item: T, query: string) => boolean) | null` (removed `?`, added `| null`)
- `contentClassName: string` (removed `?`)
- `placeholder: string` (removed `?`)
- `emptyMessage: string` (removed `?`)
- `getItemBadge: ((item: T) => string | undefined) | null` (removed `?`, added `| null`)
2. **`SearchableDropdownContainer.tsx`** (implementation):
- Removed default values from destructuring (`placeholder = 'Search'`, `emptyMessage = 'No items found'`)
- Changed `if (filterItem)` to `if (filterItem !== null)`
- Changed `getItemBadge={getItemBadge}` to `getItemBadge={getItemBadge ?? undefined}`
3. **`RepoCardSimple.tsx`** (callsite):
- Added `filterItem={null}`
- Changed `selectedValue={selectedBranch}` to `selectedValue={selectedBranch ?? null}` to handle the case when `selectedBranch` is `undefined`
All ESLint errors for `SearchableDropdownContainer.tsx` are now resolved, and TypeScript compiles without errors.
* Refactor FileTreeContainer to fix ESLint errors for optional props
- Make all props required in FileTreeContainerProps interface
- Add guard in RightSidebar to only render Changes section when
selectedWorkspace exists, ensuring workspaceId is always defined
- Remove redundant null check for onSelectFile callback
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor NewDisplayConversationEntry to fix ESLint errors for optional props
- Remove dead `task` prop from NewDisplayConversationEntry and ConversationListContainer
(was only passed to legacy DisplayConversationEntry for entry types that don't use it)
- Make `executionProcessId` and `taskAttempt` required props
- Convert internal helper component optional props from `prop?: T` to `prop: T | undefined`
to satisfy ESLint while preserving the same runtime behavior
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor VirtualizedProcessLogs to fix ESLint errors for optional props
Made search-related props required instead of optional:
- searchQuery: string (was optional)
- matchIndices: number[] (was optional)
- currentMatchIndex: number (was optional)
Updated callsites to provide explicit "no search" values where search
functionality is not used.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Cleanup script changes for workspace f4ec6f6e-a524-4760-889a-7ec2ebbce4c6
* Refactor container components to fix ESLint errors for optional props
Made the following props required (parents always provide them):
- BrowseRepoButtonContainer: disabled
- CopyButton: disabled
- WorkspacesMainContainer: isNewSessionMode, onStartNewSession
- LogsContentContainer: className
- PreviewBrowserContainer: attemptId, className
- PreviewControlsContainer: attemptId, className
Changed to union types (legitimately optional values):
- LogsContentContainer: command: string | undefined
- ProjectSelectorContainer: selectedProjectName: string | undefined
Added guards in parent components to only render children when
required data is available (RightSidebar, WorkspacesLayout).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Cleanup script changes for workspace f4ec6f6e-a524-4760-889a-7ec2ebbce4c6
* Refactor WorkspacesLayout to fix ESLint errors for optional props
Extract create mode sections into dedicated container components:
- CreateModeProjectSectionContainer: project selector with useCreateMode
- CreateModeReposSectionContainer: repos list with branch auto-selection
- CreateModeAddReposSectionContainer: add repos section
This removes the ModeProviderProps interface with optional props that
violated the ESLint rule. Now CreateModeProvider is only rendered when
in create mode, and each container manages its own context access.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Cleanup script changes for workspace f4ec6f6e-a524-4760-889a-7ec2ebbce4c6
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
328 lines
11 KiB
JavaScript
328 lines
11 KiB
JavaScript
const path = require('path');
|
|
|
|
const i18nCheck = process.env.LINT_I18N === 'true';
|
|
|
|
// Presentational components - these must be stateless and receive all data via props
|
|
const presentationalComponentPatterns = [
|
|
'src/components/ui-new/views/**/*.tsx',
|
|
'src/components/ui-new/primitives/**/*.tsx',
|
|
];
|
|
|
|
module.exports = {
|
|
root: true,
|
|
env: {
|
|
browser: true,
|
|
es2020: true,
|
|
},
|
|
extends: [
|
|
'eslint:recommended',
|
|
'plugin:@typescript-eslint/recommended',
|
|
'plugin:react-hooks/recommended',
|
|
'plugin:i18next/recommended',
|
|
'plugin:eslint-comments/recommended',
|
|
'prettier',
|
|
],
|
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
|
parser: '@typescript-eslint/parser',
|
|
plugins: ['react-refresh', '@typescript-eslint', 'unused-imports', 'i18next', 'eslint-comments', 'check-file', 'deprecation'],
|
|
parserOptions: {
|
|
ecmaVersion: 'latest',
|
|
sourceType: 'module',
|
|
project: path.join(__dirname, 'tsconfig.json'),
|
|
},
|
|
rules: {
|
|
'eslint-comments/no-use': ['error', { allow: [] }],
|
|
'react-refresh/only-export-components': 'off',
|
|
'unused-imports/no-unused-imports': 'error',
|
|
'unused-imports/no-unused-vars': [
|
|
'error',
|
|
{
|
|
vars: 'all',
|
|
args: 'after-used',
|
|
ignoreRestSiblings: false,
|
|
},
|
|
],
|
|
'@typescript-eslint/no-explicit-any': 'warn',
|
|
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
|
// Enforce typesafe modal pattern
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
paths: [
|
|
{
|
|
name: '@ebay/nice-modal-react',
|
|
importNames: ['default'],
|
|
message:
|
|
'Import NiceModal only in lib/modals.ts or dialog component files. Use DialogName.show(props) instead.',
|
|
},
|
|
{
|
|
name: '@/lib/modals',
|
|
importNames: ['showModal', 'hideModal', 'removeModal'],
|
|
message:
|
|
'Do not import showModal/hideModal/removeModal. Use DialogName.show(props) and DialogName.hide() instead.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
{
|
|
selector:
|
|
'CallExpression[callee.object.name="NiceModal"][callee.property.name="show"]',
|
|
message:
|
|
'Do not use NiceModal.show() directly. Use DialogName.show(props) instead.',
|
|
},
|
|
{
|
|
selector:
|
|
'CallExpression[callee.object.name="NiceModal"][callee.property.name="register"]',
|
|
message:
|
|
'Do not use NiceModal.register(). Dialogs are registered automatically.',
|
|
},
|
|
{
|
|
selector: 'CallExpression[callee.name="showModal"]',
|
|
message:
|
|
'Do not use showModal(). Use DialogName.show(props) instead.',
|
|
},
|
|
{
|
|
selector: 'CallExpression[callee.name="hideModal"]',
|
|
message: 'Do not use hideModal(). Use DialogName.hide() instead.',
|
|
},
|
|
{
|
|
selector: 'CallExpression[callee.name="removeModal"]',
|
|
message: 'Do not use removeModal(). Use DialogName.remove() instead.',
|
|
},
|
|
],
|
|
// i18n rule - only active when LINT_I18N=true
|
|
'i18next/no-literal-string': i18nCheck
|
|
? [
|
|
'warn',
|
|
{
|
|
markupOnly: true,
|
|
ignoreAttribute: [
|
|
'data-testid',
|
|
'to',
|
|
'href',
|
|
'id',
|
|
'key',
|
|
'type',
|
|
'role',
|
|
'className',
|
|
'style',
|
|
'aria-describedby',
|
|
],
|
|
'jsx-components': {
|
|
exclude: ['code'],
|
|
},
|
|
},
|
|
]
|
|
: 'off',
|
|
// File naming conventions
|
|
'check-file/filename-naming-convention': [
|
|
'error',
|
|
{
|
|
// React components (tsx) should be PascalCase
|
|
'src/**/*.tsx': 'PASCAL_CASE',
|
|
// Hooks should be camelCase starting with 'use'
|
|
'src/**/use*.ts': 'CAMEL_CASE',
|
|
// Utils should be camelCase
|
|
'src/utils/**/*.ts': 'CAMEL_CASE',
|
|
// Lib/config/constants should be camelCase
|
|
'src/lib/**/*.ts': 'CAMEL_CASE',
|
|
'src/config/**/*.ts': 'CAMEL_CASE',
|
|
'src/constants/**/*.ts': 'CAMEL_CASE',
|
|
},
|
|
{
|
|
ignoreMiddleExtensions: true,
|
|
},
|
|
],
|
|
},
|
|
overrides: [
|
|
{
|
|
// Entry point exception - main.tsx can stay lowercase
|
|
files: ['src/main.tsx', 'src/vite-env.d.ts'],
|
|
rules: {
|
|
'check-file/filename-naming-convention': 'off',
|
|
},
|
|
},
|
|
{
|
|
// Shadcn UI components are an exception - keep kebab-case
|
|
files: ['src/components/ui/**/*.{ts,tsx}'],
|
|
rules: {
|
|
'check-file/filename-naming-convention': [
|
|
'error',
|
|
{
|
|
'src/components/ui/**/*.{ts,tsx}': 'KEBAB_CASE',
|
|
},
|
|
{
|
|
ignoreMiddleExtensions: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
files: ['**/*.test.{ts,tsx}', '**/*.stories.{ts,tsx}'],
|
|
rules: {
|
|
'i18next/no-literal-string': 'off',
|
|
},
|
|
},
|
|
{
|
|
// Disable type-aware linting for config files
|
|
files: ['*.config.{ts,js,cjs,mjs}', '.eslintrc.cjs'],
|
|
parserOptions: {
|
|
project: null,
|
|
},
|
|
rules: {
|
|
'@typescript-eslint/switch-exhaustiveness-check': 'off',
|
|
},
|
|
},
|
|
{
|
|
// Allow NiceModal usage in lib/modals.ts, design scope files (for Provider), and dialog component files
|
|
files: [
|
|
'src/lib/modals.ts',
|
|
'src/components/legacy-design/LegacyDesignScope.tsx',
|
|
'src/components/ui-new/scope/NewDesignScope.tsx',
|
|
'src/components/dialogs/**/*.{ts,tsx}',
|
|
],
|
|
rules: {
|
|
'no-restricted-imports': 'off',
|
|
'no-restricted-syntax': 'off',
|
|
},
|
|
},
|
|
{
|
|
// ui-new components must use Phosphor icons (not Lucide) and avoid deprecated APIs
|
|
files: ['src/components/ui-new/**/*.{ts,tsx}'],
|
|
rules: {
|
|
'deprecation/deprecation': 'error',
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
paths: [
|
|
{
|
|
name: 'lucide-react',
|
|
message: 'Use @phosphor-icons/react instead of lucide-react in ui-new components.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
// Icon size restrictions - use Tailwind design system sizes
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
{
|
|
selector: 'JSXAttribute[name.name="size"][value.type="JSXExpressionContainer"]',
|
|
message:
|
|
'Icons should use Tailwind size classes (size-icon-xs, size-icon-sm, size-icon-base, size-icon-lg, size-icon-xl) instead of the size prop. Example: <Icon className="size-icon-base" />',
|
|
},
|
|
{
|
|
// Catch arbitrary pixel sizes like size-[10px], size-[7px], etc. in className
|
|
selector: 'Literal[value=/size-\\[\\d+px\\]/]',
|
|
message:
|
|
'Use standard icon sizes (size-icon-xs, size-icon-sm, size-icon-base, size-icon-lg, size-icon-xl) instead of arbitrary pixel values like size-[Npx].',
|
|
},
|
|
{
|
|
// Catch generic tailwind sizes like size-1, size-3, size-1.5, etc. (not size-icon-* or size-dot)
|
|
selector: 'Literal[value=/(?<!icon-)(?<!-)size-[0-9]/]',
|
|
message:
|
|
'Use design system sizes (size-icon-xs, size-icon-sm, size-icon-base, size-icon-lg, size-icon-xl, size-dot) instead of generic Tailwind sizes.',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
// Container components should not have optional props
|
|
files: ['src/components/ui-new/containers/**/*.{ts,tsx}'],
|
|
rules: {
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
{
|
|
selector: 'TSPropertySignature[optional=true]',
|
|
message:
|
|
'Optional props are not allowed in container components. Make the prop required or provide a default value.',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
// Logic hooks in ui-new/hooks/ - no JSX allowed
|
|
files: ['src/components/ui-new/hooks/**/*.{ts,tsx}'],
|
|
rules: {
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
{
|
|
selector: 'JSXElement',
|
|
message: 'Logic hooks must not contain JSX. Return data and callbacks only.',
|
|
},
|
|
{
|
|
selector: 'JSXFragment',
|
|
message: 'Logic hooks must not contain JSX fragments.',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
// Presentational components (views & primitives) - strict presentation rules (no logic)
|
|
files: presentationalComponentPatterns,
|
|
rules: {
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
paths: [
|
|
{
|
|
name: '@/lib/api',
|
|
message: 'Presentational components cannot import API. Pass data via props.',
|
|
},
|
|
{
|
|
name: '@tanstack/react-query',
|
|
importNames: ['useQuery', 'useMutation', 'useQueryClient', 'useInfiniteQuery'],
|
|
message: 'Presentational components cannot use data fetching hooks. Pass data via props.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
{
|
|
selector: 'CallExpression[callee.name="useState"]',
|
|
message: 'Presentational components should not manage state. Use controlled props.',
|
|
},
|
|
{
|
|
selector: 'CallExpression[callee.name="useReducer"]',
|
|
message: 'Presentational components should not use useReducer. Use container component.',
|
|
},
|
|
{
|
|
selector: 'CallExpression[callee.name="useContext"]',
|
|
message: 'Presentational components should not consume context. Pass data via props.',
|
|
},
|
|
{
|
|
selector: 'CallExpression[callee.name="useQuery"]',
|
|
message: 'Presentational components should not fetch data. Pass data via props.',
|
|
},
|
|
{
|
|
selector: 'CallExpression[callee.name="useMutation"]',
|
|
message: 'Presentational components should not mutate data. Pass callbacks via props.',
|
|
},
|
|
{
|
|
selector: 'CallExpression[callee.name="useInfiniteQuery"]',
|
|
message: 'Presentational components should not fetch data. Pass data via props.',
|
|
},
|
|
{
|
|
selector: 'CallExpression[callee.name="useEffect"]',
|
|
message: 'Presentational components should avoid side effects. Move to container.',
|
|
},
|
|
{
|
|
selector: 'CallExpression[callee.name="useLayoutEffect"]',
|
|
message: 'Presentational components should avoid layout effects. Move to container.',
|
|
},
|
|
{
|
|
selector: 'CallExpression[callee.name="useCallback"]',
|
|
message: 'Presentational components should receive callbacks via props.',
|
|
},
|
|
{
|
|
selector: 'CallExpression[callee.name="useNavigate"]',
|
|
message: 'Presentational components should not handle navigation. Pass callbacks via props.',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|