From b2fc2f60f1f816db146ba47da735f57cafe70aaa Mon Sep 17 00:00:00 2001 From: catlog22 Date: Mon, 9 Mar 2026 14:43:21 +0800 Subject: [PATCH] feat: implement ignore patterns and extension filters in CodexLens - Added tests to ensure loading of ignore patterns and extension filters from settings. - Implemented functionality to respect ignore patterns and extension filters during file indexing. - Created integration tests for CodexLens ignore-pattern configuration routes. - Added a new AdvancedTab component with tests for managing ignore patterns and extension filters. - Established a comprehensive branding naming system for the Maestro project, including guidelines for package names, CLI commands, and directory structure. --- .codex/skills/team-planex/SKILL.md | 37 -- .../components/codexlens/AdvancedTab.test.tsx | 154 +++++++ .../src/components/codexlens/AdvancedTab.tsx | 388 +++++++++++++++- .../src/components/mcp/McpServerDialog.tsx | 166 ++++++- ccw/frontend/src/lib/api.mcp.test.ts | 41 +- ccw/frontend/src/locales/en/mcp-manager.json | 14 + ccw/frontend/src/locales/zh/mcp-manager.json | 14 + .../codexlens-ignore-pattern-routes.test.ts | 144 ++++++ ccw/undefined/settings.json | 10 + .../codexlens-home/settings.json | 1 + .../frontend/app.ts | 1 + .../frontend/bundle.min.js | 1 + .../frontend/dist/compiled.ts | 1 + .../frontend/dist/bundle.ts | 1 + .../frontend/src/app.ts | 1 + .../.next/generated.py | 1 + .../.parcel-cache/generated.py | 1 + .../.turbo/generated.py | 1 + .../build/generated.py | 1 + .../coverage/generated.py | 1 + .../dist/generated.py | 1 + .../out/generated.py | 1 + .../src/app.py | 1 + .../target/generated.py | 1 + .../frontend/app.ts | 1 + .../frontend/bundle.min.js | 1 + .../frontend/skip.ts | 1 + .../settings.json | 1 + .../package/dist/bundle.py | 1 + .../src/codexlens/storage/index_tree.py | 93 +++- .../tests/test_config_ignore_patterns.py | 1 + .../tests/test_index_tree_ignore_dirs.py | 61 +++ docs/branding/naming-system.md | 415 ++++++++++++++++++ 33 files changed, 1489 insertions(+), 69 deletions(-) create mode 100644 ccw/frontend/src/components/codexlens/AdvancedTab.test.tsx create mode 100644 ccw/tests/integration/codexlens-ignore-pattern-routes.test.ts create mode 100644 ccw/undefined/settings.json create mode 100644 codex-lens/.pytest-temp/test_builder_loads_saved_ignor0/codexlens-home/settings.json create mode 100644 codex-lens/.pytest-temp/test_builder_loads_saved_ignor0/frontend/app.ts create mode 100644 codex-lens/.pytest-temp/test_builder_loads_saved_ignor0/frontend/bundle.min.js create mode 100644 codex-lens/.pytest-temp/test_builder_loads_saved_ignor0/frontend/dist/compiled.ts create mode 100644 codex-lens/.pytest-temp/test_collect_dirs_by_depth_res0/frontend/dist/bundle.ts create mode 100644 codex-lens/.pytest-temp/test_collect_dirs_by_depth_res0/frontend/src/app.ts create mode 100644 codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/.next/generated.py create mode 100644 codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/.parcel-cache/generated.py create mode 100644 codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/.turbo/generated.py create mode 100644 codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/build/generated.py create mode 100644 codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/coverage/generated.py create mode 100644 codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/dist/generated.py create mode 100644 codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/out/generated.py create mode 100644 codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/src/app.py create mode 100644 codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/target/generated.py create mode 100644 codex-lens/.pytest-temp/test_iter_source_files_respect0/frontend/app.ts create mode 100644 codex-lens/.pytest-temp/test_iter_source_files_respect0/frontend/bundle.min.js create mode 100644 codex-lens/.pytest-temp/test_iter_source_files_respect0/frontend/skip.ts create mode 100644 codex-lens/.pytest-temp/test_load_settings_reads_ignor0/settings.json create mode 100644 codex-lens/.pytest-temp/test_should_index_dir_ignores_0/package/dist/bundle.py create mode 100644 docs/branding/naming-system.md diff --git a/.codex/skills/team-planex/SKILL.md b/.codex/skills/team-planex/SKILL.md index a7de7641..5c1bb6d2 100644 --- a/.codex/skills/team-planex/SKILL.md +++ b/.codex/skills/team-planex/SKILL.md @@ -502,40 +502,3 @@ if (plannerAgent) { | `check` / `status` | Show progress: planned / executing / completed / failed counts | | `resume` / `continue` | Re-enter loop from Phase 2 | - ---- - -## Coordinator Role Constraints (Main Agent) - -**CRITICAL**: The coordinator (main agent executing this skill) is responsible for **orchestration only**, NOT implementation. - -15. **Coordinator Does NOT Execute Code**: The main agent MUST NOT write, modify, or implement any code directly. All implementation work is delegated to spawned team agents. The coordinator only: - - Spawns agents with task assignments - - Waits for agent callbacks - - Merges results and coordinates workflow - - Manages workflow transitions between phases - -16. **Patient Waiting is Mandatory**: Agent execution takes significant time (typically 10-30 minutes per phase, sometimes longer). The coordinator MUST: - - Wait patiently for `wait()` calls to complete - - NOT skip workflow steps due to perceived delays - - NOT assume agents have failed just because they're taking time - - Trust the timeout mechanisms defined in the skill - -17. **Use send_input for Clarification**: When agents need guidance or appear stuck, the coordinator MUST: - - Use `send_input()` to ask questions or provide clarification - - NOT skip the agent or move to next phase prematurely - - Give agents opportunity to respond before escalating - - Example: `send_input({ id: agent_id, message: "Please provide status update or clarify blockers" })` - -18. **No Workflow Shortcuts**: The coordinator MUST NOT: - - Skip phases or stages defined in the workflow - - Bypass required approval or review steps - - Execute dependent tasks before prerequisites complete - - Assume task completion without explicit agent callback - - Make up or fabricate agent results - -19. **Respect Long-Running Processes**: This is a complex multi-agent workflow that requires patience: - - Total execution time may range from 30-90 minutes or longer - - Each phase may take 10-30 minutes depending on complexity - - The coordinator must remain active and attentive throughout the entire process - - Do not terminate or skip steps due to time concerns diff --git a/ccw/frontend/src/components/codexlens/AdvancedTab.test.tsx b/ccw/frontend/src/components/codexlens/AdvancedTab.test.tsx new file mode 100644 index 00000000..e771acdf --- /dev/null +++ b/ccw/frontend/src/components/codexlens/AdvancedTab.test.tsx @@ -0,0 +1,154 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent } from '@testing-library/react'; +import { render, screen, waitFor } from '@/test/i18n'; +import { AdvancedTab } from './AdvancedTab'; + +vi.mock('@/hooks', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useCodexLensEnv: vi.fn(), + useUpdateCodexLensEnv: vi.fn(), + useCodexLensIgnorePatterns: vi.fn(), + useUpdateIgnorePatterns: vi.fn(), + useNotifications: vi.fn(), + }; +}); + +import { + useCodexLensEnv, + useUpdateCodexLensEnv, + useCodexLensIgnorePatterns, + useUpdateIgnorePatterns, + useNotifications, +} from '@/hooks'; + +const mockRefetchEnv = vi.fn().mockResolvedValue(undefined); +const mockRefetchPatterns = vi.fn().mockResolvedValue(undefined); +const mockUpdateEnv = vi.fn().mockResolvedValue({ success: true, message: 'Saved' }); +const mockUpdatePatterns = vi.fn().mockResolvedValue({ + success: true, + patterns: ['dist', 'frontend/dist'], + extensionFilters: ['*.min.js', 'frontend/skip.ts'], + defaults: { + patterns: ['dist', 'build'], + extensionFilters: ['*.min.js'], + }, +}); +const mockToastSuccess = vi.fn(); +const mockToastError = vi.fn(); + +function setupDefaultMocks() { + vi.mocked(useCodexLensEnv).mockReturnValue({ + data: { success: true, env: {}, settings: {}, raw: '', path: '~/.codexlens/.env' }, + raw: '', + env: {}, + settings: {}, + isLoading: false, + error: null, + refetch: mockRefetchEnv, + }); + + vi.mocked(useUpdateCodexLensEnv).mockReturnValue({ + updateEnv: mockUpdateEnv, + isUpdating: false, + error: null, + }); + + vi.mocked(useCodexLensIgnorePatterns).mockReturnValue({ + data: { + success: true, + patterns: ['dist', 'coverage'], + extensionFilters: ['*.min.js', '*.map'], + defaults: { + patterns: ['dist', 'build', 'coverage'], + extensionFilters: ['*.min.js', '*.map'], + }, + }, + patterns: ['dist', 'coverage'], + extensionFilters: ['*.min.js', '*.map'], + defaults: { + patterns: ['dist', 'build', 'coverage'], + extensionFilters: ['*.min.js', '*.map'], + }, + isLoading: false, + error: null, + refetch: mockRefetchPatterns, + }); + + vi.mocked(useUpdateIgnorePatterns).mockReturnValue({ + updatePatterns: mockUpdatePatterns, + isUpdating: false, + error: null, + }); + + vi.mocked(useNotifications).mockReturnValue({ + success: mockToastSuccess, + error: mockToastError, + } as ReturnType); +} + +describe('AdvancedTab', () => { + beforeEach(() => { + vi.clearAllMocks(); + setupDefaultMocks(); + }); + + it('renders existing filter configuration', () => { + render(); + + expect(screen.getByLabelText(/Ignored directories \/ paths/i)).toHaveValue('dist\ncoverage'); + expect(screen.getByLabelText(/Skipped files \/ globs/i)).toHaveValue('*.min.js\n*.map'); + expect(screen.getByText(/Directory filters: 2/i)).toBeInTheDocument(); + expect(screen.getByText(/File filters: 2/i)).toBeInTheDocument(); + }); + + it('saves parsed filter configuration', async () => { + render(); + + const ignorePatternsInput = screen.getByLabelText(/Ignored directories \/ paths/i); + const extensionFiltersInput = screen.getByLabelText(/Skipped files \/ globs/i); + + fireEvent.change(ignorePatternsInput, { target: { value: 'dist,\nfrontend/dist' } }); + fireEvent.change(extensionFiltersInput, { target: { value: '*.min.js,\nfrontend/skip.ts' } }); + fireEvent.click(screen.getByRole('button', { name: /Save filters/i })); + + await waitFor(() => { + expect(mockUpdatePatterns).toHaveBeenCalledWith({ + patterns: ['dist', 'frontend/dist'], + extensionFilters: ['*.min.js', 'frontend/skip.ts'], + }); + }); + expect(mockRefetchPatterns).toHaveBeenCalled(); + }); + + it('restores default filter values before saving', async () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: /Restore defaults/i })); + + expect(screen.getByLabelText(/Ignored directories \/ paths/i)).toHaveValue('dist\nbuild\ncoverage'); + expect(screen.getByLabelText(/Skipped files \/ globs/i)).toHaveValue('*.min.js\n*.map'); + + fireEvent.click(screen.getByRole('button', { name: /Save filters/i })); + + await waitFor(() => { + expect(mockUpdatePatterns).toHaveBeenCalledWith({ + patterns: ['dist', 'build', 'coverage'], + extensionFilters: ['*.min.js', '*.map'], + }); + }); + }); + + it('blocks invalid filter entries before saving', async () => { + render(); + + fireEvent.change(screen.getByLabelText(/Ignored directories \/ paths/i), { + target: { value: 'bad pattern!' }, + }); + fireEvent.click(screen.getByRole('button', { name: /Save filters/i })); + + expect(mockUpdatePatterns).not.toHaveBeenCalled(); + expect(screen.getByText(/Invalid ignore patterns/i)).toBeInTheDocument(); + }); +}); diff --git a/ccw/frontend/src/components/codexlens/AdvancedTab.tsx b/ccw/frontend/src/components/codexlens/AdvancedTab.tsx index 06274da7..e19bb070 100644 --- a/ccw/frontend/src/components/codexlens/AdvancedTab.tsx +++ b/ccw/frontend/src/components/codexlens/AdvancedTab.tsx @@ -5,13 +5,18 @@ import { useState, useEffect } from 'react'; import { useIntl } from 'react-intl'; -import { Save, RefreshCw, AlertTriangle, FileCode, AlertCircle } from 'lucide-react'; +import { Save, RefreshCw, AlertTriangle, FileCode, AlertCircle, Filter } from 'lucide-react'; import { Card } from '@/components/ui/Card'; import { Textarea } from '@/components/ui/Textarea'; import { Button } from '@/components/ui/Button'; import { Label } from '@/components/ui/Label'; import { Badge } from '@/components/ui/Badge'; -import { useCodexLensEnv, useUpdateCodexLensEnv } from '@/hooks'; +import { + useCodexLensEnv, + useCodexLensIgnorePatterns, + useUpdateCodexLensEnv, + useUpdateIgnorePatterns, +} from '@/hooks'; import { useNotifications } from '@/hooks'; import { cn } from '@/lib/utils'; import { CcwToolsCard } from './CcwToolsCard'; @@ -22,6 +27,28 @@ interface AdvancedTabProps { interface FormErrors { env?: string; + ignorePatterns?: string; + extensionFilters?: string; +} + +const FILTER_ENTRY_PATTERN = /^[-\w.*\\/]+$/; + +function parseListEntries(text: string): string[] { + return text + .split(/[\n,]/) + .map((entry) => entry.trim()) + .filter(Boolean); +} + +function normalizeListEntries(entries: string[]): string { + return entries + .map((entry) => entry.trim()) + .filter(Boolean) + .join('\n'); +} + +function normalizeListText(text: string): string { + return normalizeListEntries(parseListEntries(text)); } export function AdvancedTab({ enabled = true }: AdvancedTabProps) { @@ -37,14 +64,32 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) { refetch, } = useCodexLensEnv({ enabled }); + const { + patterns, + extensionFilters, + defaults, + isLoading: isLoadingPatterns, + error: patternsError, + refetch: refetchPatterns, + } = useCodexLensIgnorePatterns({ enabled }); + const { updateEnv, isUpdating } = useUpdateCodexLensEnv(); + const { updatePatterns, isUpdating: isUpdatingPatterns } = useUpdateIgnorePatterns(); // Form state const [envInput, setEnvInput] = useState(''); + const [ignorePatternsInput, setIgnorePatternsInput] = useState(''); + const [extensionFiltersInput, setExtensionFiltersInput] = useState(''); const [errors, setErrors] = useState({}); const [hasChanges, setHasChanges] = useState(false); + const [hasFilterChanges, setHasFilterChanges] = useState(false); const [showWarning, setShowWarning] = useState(false); + const currentIgnorePatterns = patterns ?? []; + const currentExtensionFilters = extensionFilters ?? []; + const defaultIgnorePatterns = defaults?.patterns ?? []; + const defaultExtensionFilters = defaults?.extensionFilters ?? []; + // Initialize form from env - handles both undefined (loading) and empty string (empty file) // The hook returns raw directly, so we check if it's been set (not undefined means data loaded) useEffect(() => { @@ -58,6 +103,31 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) { } }, [raw, isLoadingEnv]); + useEffect(() => { + if (!isLoadingPatterns) { + const nextIgnorePatterns = patterns ?? []; + const nextExtensionFilters = extensionFilters ?? []; + setIgnorePatternsInput(nextIgnorePatterns.join('\n')); + setExtensionFiltersInput(nextExtensionFilters.join('\n')); + setErrors((prev) => ({ + ...prev, + ignorePatterns: undefined, + extensionFilters: undefined, + })); + setHasFilterChanges(false); + } + }, [extensionFilters, isLoadingPatterns, patterns]); + + const updateFilterChangeState = (nextIgnorePatternsInput: string, nextExtensionFiltersInput: string) => { + const normalizedCurrentIgnorePatterns = normalizeListEntries(currentIgnorePatterns); + const normalizedCurrentExtensionFilters = normalizeListEntries(currentExtensionFilters); + + setHasFilterChanges( + normalizeListText(nextIgnorePatternsInput) !== normalizedCurrentIgnorePatterns + || normalizeListText(nextExtensionFiltersInput) !== normalizedCurrentExtensionFilters + ); + }; + const handleEnvChange = (value: string) => { setEnvInput(value); // Check if there are changes - compare with raw value (handle undefined as empty) @@ -69,6 +139,22 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) { } }; + const handleIgnorePatternsChange = (value: string) => { + setIgnorePatternsInput(value); + updateFilterChangeState(value, extensionFiltersInput); + if (errors.ignorePatterns) { + setErrors((prev) => ({ ...prev, ignorePatterns: undefined })); + } + }; + + const handleExtensionFiltersChange = (value: string) => { + setExtensionFiltersInput(value); + updateFilterChangeState(ignorePatternsInput, value); + if (errors.extensionFilters) { + setErrors((prev) => ({ ...prev, extensionFilters: undefined })); + } + }; + const parseEnvVariables = (text: string): Record => { const envObj: Record = {}; const lines = text.split('\n'); @@ -101,10 +187,50 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) { ); } - setErrors(newErrors); + setErrors((prev) => ({ ...prev, env: newErrors.env })); return Object.keys(newErrors).length === 0; }; + const validateFilterForm = (): boolean => { + const nextErrors: Pick = {}; + const parsedIgnorePatterns = parseListEntries(ignorePatternsInput); + const parsedExtensionFilters = parseListEntries(extensionFiltersInput); + + const invalidIgnorePatterns = parsedIgnorePatterns.filter( + (entry) => !FILTER_ENTRY_PATTERN.test(entry) + ); + if (invalidIgnorePatterns.length > 0) { + nextErrors.ignorePatterns = formatMessage( + { + id: 'codexlens.advanced.validation.invalidIgnorePatterns', + defaultMessage: 'Invalid ignore patterns: {values}', + }, + { values: invalidIgnorePatterns.join(', ') } + ); + } + + const invalidExtensionFilters = parsedExtensionFilters.filter( + (entry) => !FILTER_ENTRY_PATTERN.test(entry) + ); + if (invalidExtensionFilters.length > 0) { + nextErrors.extensionFilters = formatMessage( + { + id: 'codexlens.advanced.validation.invalidExtensionFilters', + defaultMessage: 'Invalid file filters: {values}', + }, + { values: invalidExtensionFilters.join(', ') } + ); + } + + setErrors((prev) => ({ + ...prev, + ignorePatterns: nextErrors.ignorePatterns, + extensionFilters: nextErrors.extensionFilters, + })); + + return Object.values(nextErrors).every((value) => !value); + }; + const handleSave = async () => { if (!validateForm()) { return; @@ -138,12 +264,68 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) { const handleReset = () => { // Reset to current raw value (handle undefined as empty) setEnvInput(raw ?? ''); - setErrors({}); + setErrors((prev) => ({ ...prev, env: undefined })); setHasChanges(false); setShowWarning(false); }; + const handleSaveFilters = async () => { + if (!validateFilterForm()) { + return; + } + + const parsedIgnorePatterns = parseListEntries(ignorePatternsInput); + const parsedExtensionFilters = parseListEntries(extensionFiltersInput); + + try { + const result = await updatePatterns({ + patterns: parsedIgnorePatterns, + extensionFilters: parsedExtensionFilters, + }); + + if (result.success) { + setIgnorePatternsInput((result.patterns ?? parsedIgnorePatterns).join('\n')); + setExtensionFiltersInput((result.extensionFilters ?? parsedExtensionFilters).join('\n')); + setHasFilterChanges(false); + setErrors((prev) => ({ + ...prev, + ignorePatterns: undefined, + extensionFilters: undefined, + })); + await refetchPatterns(); + } + } catch { + // Hook-level mutation already reports the error. + } + }; + + const handleResetFilters = () => { + setIgnorePatternsInput(currentIgnorePatterns.join('\n')); + setExtensionFiltersInput(currentExtensionFilters.join('\n')); + setErrors((prev) => ({ + ...prev, + ignorePatterns: undefined, + extensionFilters: undefined, + })); + setHasFilterChanges(false); + }; + + const handleRestoreDefaultFilters = () => { + const defaultIgnoreText = defaultIgnorePatterns.join('\n'); + const defaultExtensionText = defaultExtensionFilters.join('\n'); + + setIgnorePatternsInput(defaultIgnoreText); + setExtensionFiltersInput(defaultExtensionText); + setErrors((prev) => ({ + ...prev, + ignorePatterns: undefined, + extensionFilters: undefined, + })); + updateFilterChangeState(defaultIgnoreText, defaultExtensionText); + }; + const isLoading = isLoadingEnv; + const isLoadingFilters = isLoadingPatterns; // Get current env variables as array for display const currentEnvVars = env @@ -242,6 +424,204 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) { {/* CCW Tools Card */} + {/* Index Filters */} + +
+
+ +

+ {formatMessage({ + id: 'codexlens.advanced.indexFilters', + defaultMessage: 'Index Filters', + })} +

+
+
+ + {formatMessage( + { + id: 'codexlens.advanced.ignorePatternCount', + defaultMessage: 'Directory filters: {count}', + }, + { count: currentIgnorePatterns.length } + )} + + + {formatMessage( + { + id: 'codexlens.advanced.extensionFilterCount', + defaultMessage: 'File filters: {count}', + }, + { count: currentExtensionFilters.length } + )} + +
+
+ +
+ {patternsError && ( +
+
+ +
+

+ {formatMessage({ + id: 'codexlens.advanced.filtersLoadError', + defaultMessage: 'Unable to load current filter settings', + })} +

+

{patternsError.message}

+
+
+
+ )} + +
+
+ +