From 1e6fcfb5bb33e436c8595e3db000f4f80cc2cacf Mon Sep 17 00:00:00 2001 From: Mehdi Date: Sun, 8 Feb 2026 18:30:19 +0000 Subject: [PATCH 1/3] fix(filters): restore default include/exclude behavior --- src/main/index.ts | 5 ++- src/utils/filter-utils.ts | 23 +++++----- tests/unit/utils/filter-utils.test.ts | 61 ++++++++++++++++----------- 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index feba3cd..9225cb6 100755 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -101,6 +101,8 @@ app.on('activate', () => { // IPC Event Handlers +type FilterPatternBundle = string[] & { includePatterns?: string[]; includeExtensions?: string[] }; + // Select directory dialog ipcMain.handle('dialog:selectDirectory', async () => { const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow ?? undefined, { @@ -123,8 +125,7 @@ ipcMain.handle( // in the UI tree view. This is critical for performance with large repositories. // Parse config to get settings and exclude patterns - let excludePatterns: (string[] & { includePatterns?: string[]; includeExtensions?: string[] }) = - ['']; + let excludePatterns: FilterPatternBundle = []; try { const config = (configContent ? (yaml.parse(configContent) as ConfigObject) diff --git a/src/utils/filter-utils.ts b/src/utils/filter-utils.ts index f96fa96..5b4da0b 100644 --- a/src/utils/filter-utils.ts +++ b/src/utils/filter-utils.ts @@ -10,14 +10,18 @@ export const getRelativePath = (filePath: string, rootPath: string): string => normalizePath(path.relative(rootPath, filePath)); const shouldExcludeByExtension = (itemPath: string, config?: ConfigObject): boolean => { + const useCustomIncludes = config?.use_custom_includes !== false; + if ( - config?.use_custom_includes && + useCustomIncludes && config?.include_extensions && Array.isArray(config.include_extensions) && + config.include_extensions.length > 0 && path.extname(itemPath) ) { const ext = path.extname(itemPath).toLowerCase(); - return !config.include_extensions.includes(ext); + const includeExtensions = config.include_extensions.map((includeExt) => includeExt.toLowerCase()); + return !includeExtensions.includes(ext); } return false; @@ -66,19 +70,16 @@ export const shouldExclude = ( try { const itemName = path.basename(itemPath); const normalizedPath = getRelativePath(itemPath, rootPath); + const useCustomExcludes = config?.use_custom_excludes !== false; + const customExcludes = + useCustomExcludes && Array.isArray(config?.exclude_patterns) ? config.exclude_patterns : []; if (shouldExcludeByExtension(itemPath, config)) { return true; } - if (config?.use_custom_excludes === true && config?.exclude_patterns) { - const customExcludes = Array.isArray(config.exclude_patterns) ? config.exclude_patterns : []; - if ( - customExcludes.length > 0 && - matchesExcludePatterns(normalizedPath, itemName, customExcludes) - ) { - return true; - } + if (customExcludes.length > 0 && matchesExcludePatterns(normalizedPath, itemName, customExcludes)) { + return true; } if (config?.use_gitignore !== false) { @@ -91,7 +92,7 @@ export const shouldExclude = ( } const gitignoreExcludes = Array.isArray(excludePatterns) - ? excludePatterns.filter((pattern) => !(config?.exclude_patterns || []).includes(pattern)) + ? excludePatterns.filter((pattern) => !customExcludes.includes(pattern)) : []; if ( diff --git a/tests/unit/utils/filter-utils.test.ts b/tests/unit/utils/filter-utils.test.ts index b4d0f95..fa9336a 100644 --- a/tests/unit/utils/filter-utils.test.ts +++ b/tests/unit/utils/filter-utils.test.ts @@ -58,7 +58,6 @@ describe('filter-utils', () => { describe('shouldExclude', () => { // Test cases for different combinations of config settings - test('should exclude files that match exclude patterns when use_custom_excludes is true', () => { const itemPath = '/project/node_modules/package.json'; const rootPath = '/project'; @@ -94,13 +93,7 @@ describe('filter-utils', () => { include_extensions: ['.js', '.jsx'], }; - // Mock implementation to ensure correct behavior for this test - jest.spyOn(path, 'extname').mockImplementationOnce(() => '.css'); - expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(true); - - // Reset the mock - path.extname.mockRestore(); }); test('should exclude files with non-matching extensions when use_custom_includes is true', () => { @@ -112,24 +105,7 @@ describe('filter-utils', () => { include_extensions: ['.js', '.jsx', '.json'], }; - // Use a direct mock replacement rather than mockReturnValueOnce - const originalExtname = path.extname; - path.extname = jest.fn().mockReturnValue('.css'); - - // Debug: Log values to understand the issue - console.log('Testing file extension exclusion:'); - console.log(`Path extname returns: ${path.extname(itemPath)}`); - console.log(`Config includes: ${config.include_extensions}`); - console.log( - `Should exclude?: ${!config.include_extensions.includes(path.extname(itemPath))}` - ); - - const result = shouldExclude(itemPath, rootPath, excludePatterns, config); - - // Restore original function - path.extname = originalExtname; - - expect(result).toBe(true); + expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(true); }); test('should include files with matching extensions when use_custom_includes is true', () => { @@ -144,6 +120,17 @@ describe('filter-utils', () => { expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(false); }); + test('should apply include extension filtering by default when use_custom_includes is undefined', () => { + const itemPath = '/project/src/file.css'; + const rootPath = '/project'; + const excludePatterns = []; + const config = { + include_extensions: ['.js', '.ts'], + }; + + expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(true); + }); + test('should exclude files that match gitignore patterns when use_gitignore is true', () => { const itemPath = '/project/logs/error.log'; const rootPath = '/project'; @@ -168,6 +155,17 @@ describe('filter-utils', () => { expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(false); }); + test('should apply custom excludes by default when use_custom_excludes is undefined', () => { + const itemPath = '/project/build/output.log'; + const rootPath = '/project'; + const excludePatterns = []; + const config = { + exclude_patterns: ['**/*.log'], + }; + + expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(true); + }); + test('should handle precedence of custom excludes over gitignore includes', () => { const itemPath = '/project/logs/important.log'; const rootPath = '/project'; @@ -202,6 +200,19 @@ describe('filter-utils', () => { expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(false); }); + test('should keep gitignore excludes active when custom excludes are disabled', () => { + const itemPath = '/project/dist/main.js'; + const rootPath = '/project'; + const excludePatterns = ['**/dist/**']; + const config = { + use_custom_excludes: false, + use_gitignore: true, + exclude_patterns: ['**/dist/**'], + }; + + expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(true); + }); + test('should handle empty patterns', () => { const itemPath = '/project/src/file.js'; const rootPath = '/project'; From 6e0b3d750316e91256532df18b60e9f8b5f2ec4c Mon Sep 17 00:00:00 2001 From: Mehdi Date: Sun, 8 Feb 2026 18:45:15 +0000 Subject: [PATCH 2/3] fix(ui): restore responsive pane sizing and add cross-platform QA screenshots --- .github/workflows/qa-matrix.yml | 71 ++++++++++++++++++ package-lock.json | 48 +++++++++++++ package.json | 12 ++-- scripts/capture-ui-screenshot.js | 91 ++++++++++++++++++++++++ src/renderer/components/App.tsx | 6 +- src/renderer/components/FileTree.tsx | 4 +- src/renderer/components/ProcessedTab.tsx | 4 +- src/renderer/components/SourceTab.tsx | 6 +- src/renderer/index.html | 6 +- src/renderer/styles.css | 4 -- 10 files changed, 231 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/qa-matrix.yml create mode 100644 scripts/capture-ui-screenshot.js diff --git a/.github/workflows/qa-matrix.yml b/.github/workflows/qa-matrix.yml new file mode 100644 index 0000000..16dfb29 --- /dev/null +++ b/.github/workflows/qa-matrix.yml @@ -0,0 +1,71 @@ +name: QA Matrix + +on: + pull_request: + branches: ['main'] + push: + branches: ['main'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + qa: + name: QA (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 + with: + node-version: 20 + package-manager-cache: false + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Unit and integration tests + run: npm test -- --runInBand + + - name: Build TypeScript + run: npm run build:ts + + - name: Build CSS + run: npm run build:css + + - name: Build renderer bundle + run: npm run build:webpack + + - name: Install Playwright browser (Linux) + if: runner.os == 'Linux' + run: npx playwright install --with-deps chromium + + - name: Install Playwright browser (Windows/macOS) + if: runner.os != 'Linux' + run: npx playwright install chromium + + - name: Capture UI screenshot + run: npm run qa:screenshot + env: + UI_SCREENSHOT_NAME: ui-${{ runner.os }}.png + + - name: Upload UI screenshot + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: ui-screenshot-${{ runner.os }} + path: dist/qa/screenshots/*.png + if-no-files-found: error + retention-days: 14 diff --git a/package-lock.json b/package-lock.json index f546c67..7d80b03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.2", + "playwright": "^1.58.2", "postcss": "^8.4.35", "postcss-loader": "^8.1.0", "prettier": "^3.2.5", @@ -15399,6 +15400,53 @@ "node": ">= 0.4.0" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", diff --git a/package.json b/package.json index 6d82767..6de50b6 100755 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "prepare": "husky install", "sonar": "node scripts/sonar-scan.js", "qa": "node scripts/index.js qa", + "qa:screenshot": "node scripts/capture-ui-screenshot.js", "security": "node scripts/index.js security", "gitleaks": "node scripts/index.js gitleaks", "sbom": "node scripts/index.js sbom", @@ -127,14 +128,14 @@ "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", "@electron/rebuild": "^3.6.0", - "@types/jest": "^29.5.14", - "@types/node": "^22.13.4", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", "@jest/globals": "^29.7.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^14.3.1", "@testing-library/user-event": "^14.6.1", + "@types/jest": "^29.5.14", + "@types/node": "^22.13.4", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", "@typescript-eslint/eslint-plugin": "^8.24.0", "@typescript-eslint/parser": "^8.24.0", "autoprefixer": "^10.4.17", @@ -158,12 +159,13 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.2", + "playwright": "^1.58.2", "postcss": "^8.4.35", "postcss-loader": "^8.1.0", "prettier": "^3.2.5", "rimraf": "^5.0.5", - "style-loader": "^3.3.4", "sonarqube-scanner": "^3.3.0", + "style-loader": "^3.3.4", "tailwindcss": "^3.4.1", "typescript": "^5.7.3", "webpack": "^5.90.1", diff --git a/scripts/capture-ui-screenshot.js b/scripts/capture-ui-screenshot.js new file mode 100644 index 0000000..b5f1f8c --- /dev/null +++ b/scripts/capture-ui-screenshot.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const http = require('http'); +const path = require('path'); +const { chromium } = require('playwright'); + +const ROOT_DIR = path.join(__dirname, '..'); +const RENDERER_DIR = path.join(ROOT_DIR, 'src', 'renderer'); +const SCREENSHOT_DIR = path.join(ROOT_DIR, 'dist', 'qa', 'screenshots'); +const SCREENSHOT_NAME = + process.env.UI_SCREENSHOT_NAME || `ui-${process.platform}-${process.arch}.png`; +const SCREENSHOT_PATH = path.join(SCREENSHOT_DIR, SCREENSHOT_NAME); +const PORT = Number(process.env.UI_SCREENSHOT_PORT || 4173); + +const MIME_TYPES = { + '.css': 'text/css; charset=UTF-8', + '.html': 'text/html; charset=UTF-8', + '.js': 'application/javascript; charset=UTF-8', + '.json': 'application/json; charset=UTF-8', + '.png': 'image/png', + '.svg': 'image/svg+xml', +}; + +function resolveFilePath(requestUrl) { + const urlPath = decodeURIComponent(requestUrl.split('?')[0]); + const relativePath = urlPath === '/' ? 'index.html' : urlPath.replace(/^\/+/, ''); + const absolutePath = path.resolve(RENDERER_DIR, relativePath); + const relativeToRoot = path.relative(RENDERER_DIR, absolutePath); + + if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) { + return null; + } + + return absolutePath; +} + +function createStaticServer() { + return http.createServer((request, response) => { + const requestedPath = resolveFilePath(request.url || '/'); + + if (!requestedPath) { + response.writeHead(403, { 'Content-Type': 'text/plain; charset=UTF-8' }); + response.end('Forbidden'); + return; + } + + fs.readFile(requestedPath, (error, content) => { + if (error) { + response.writeHead(404, { 'Content-Type': 'text/plain; charset=UTF-8' }); + response.end('Not Found'); + return; + } + + const extension = path.extname(requestedPath).toLowerCase(); + const contentType = MIME_TYPES[extension] || 'application/octet-stream'; + response.writeHead(200, { 'Content-Type': contentType }); + response.end(content); + }); + }); +} + +async function captureScreenshot() { + fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); + + const server = createStaticServer(); + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(PORT, '127.0.0.1', () => resolve()); + }); + + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + + try { + await page.goto(`http://127.0.0.1:${PORT}/index.html`, { waitUntil: 'networkidle' }); + await page.waitForSelector('#app', { timeout: 10000 }); + await page.waitForTimeout(1000); + await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }); + console.log(`UI screenshot captured: ${SCREENSHOT_PATH}`); + } finally { + await page.close(); + await browser.close(); + await new Promise((resolve) => server.close(resolve)); + } +} + +captureScreenshot().catch((error) => { + console.error(`Failed to capture UI screenshot: ${error.message}`); + process.exit(1); +}); diff --git a/src/renderer/components/App.tsx b/src/renderer/components/App.tsx index 319b84c..1a0312d 100755 --- a/src/renderer/components/App.tsx +++ b/src/renderer/components/App.tsx @@ -512,9 +512,9 @@ const App = () => { return ( -
+
{/* Tab navigation and content container */} -
+
{/* Tab Bar and title in the same row */}
@@ -578,7 +578,7 @@ const App = () => {
{/* Tab content */} -
+
{activeTab === 'config' && ( )} diff --git a/src/renderer/components/FileTree.tsx b/src/renderer/components/FileTree.tsx index 6cf584b..0be2116 100755 --- a/src/renderer/components/FileTree.tsx +++ b/src/renderer/components/FileTree.tsx @@ -272,7 +272,7 @@ const FileTreeComponent = ({ }; return ( -
+
-
+
{items.length === 0 ? (
                 {processedResult.content}
@@ -211,7 +211,7 @@ const ProcessedTab = ({ processedResult, onSave, onRefresh }: ProcessedTabProps)
               Files by Token Count
             
             
-
+
diff --git a/src/renderer/components/SourceTab.tsx b/src/renderer/components/SourceTab.tsx index 241a55e..7e1877f 100755 --- a/src/renderer/components/SourceTab.tsx +++ b/src/renderer/components/SourceTab.tsx @@ -176,7 +176,7 @@ const SourceTab = ({ }, []); return ( -
+
{directoryTree.length > 0 ? ( -
+