Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/qa-matrix.yml
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -158,12 +159,13 @@
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.2.2",
"playwright": "^1.58.2",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

playwright npm package latest version 2026

💡 Result:

As of February 8, 2026, the latest stable version of the playwright npm package is 1.58.1. [1]

If you’re looking at pre-releases, there are newer 1.59.0-alpha builds available. [2]

Sources: [1] [2]


Update playwright version from ^1.58.2 to ^1.58.1.

Playwright version 1.58.2 does not exist. The latest stable version as of February 2026 is 1.58.1.

🤖 Prompt for AI Agents
In `@package.json` at line 162, The package.json lists "playwright": "^1.58.2"
which is invalid because 1.58.2 doesn't exist; update the dependency to
"playwright": "^1.58.1" in the package.json (locate the "playwright" entry) and
then run your package manager (npm install or yarn install) to update lockfiles
and ensure consistency.

"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",
Expand Down
91 changes: 91 additions & 0 deletions scripts/capture-ui-screenshot.js
Original file line number Diff line number Diff line change
@@ -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);
Comment on lines +11 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. ui_screenshot_name path not sanitized 📘 Rule violation ⛨ Security

SCREENSHOT_NAME is taken directly from process.env.UI_SCREENSHOT_NAME and used in
  path.join(SCREENSHOT_DIR, SCREENSHOT_NAME), allowing path traversal (e.g., ../...) and
  unintended writes outside dist/qa/screenshots.
• This violates the requirement to validate/sanitize external inputs (environment variables) and can
  create a security risk in CI runners or other environments where env vars can be influenced.
Agent Prompt
## Issue description
`scripts/capture-ui-screenshot.js` uses `process.env.UI_SCREENSHOT_NAME` directly when creating `SCREENSHOT_PATH`, which can allow path traversal and writing files outside the intended screenshots directory.

## Issue Context
Environment variables are external inputs. The script should only allow a safe filename (e.g., `ui-linux.png`) and disallow `..`, absolute paths, or path separators.

## Fix Focus Areas
- scripts/capture-ui-screenshot.js[11-14]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a fixed waitForTimeout can lead to flaky tests. If the UI takes longer than 1 second to render (e.g., on a slower machine), the screenshot might be captured prematurely. Conversely, the script waits unnecessarily if the UI renders faster.

A better practice is to wait for a specific element to be visible, which ensures the UI is ready before capturing the screenshot. This makes the test more robust and efficient.

Suggested change
await page.waitForTimeout(1000);
await page.waitForSelector('#app > div', { state: 'visible', timeout: 10000 });

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);
});
5 changes: 3 additions & 2 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -512,9 +512,9 @@ const App = () => {

return (
<DarkModeProvider>
<div className='container mx-auto p-4'>
<div className='mx-auto flex h-screen w-full max-w-screen-2xl flex-col p-4'>
{/* Tab navigation and content container */}
<div className='w-full border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 transition-colors duration-200'>
<div className='flex min-h-0 w-full flex-1 flex-col border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 transition-colors duration-200'>
{/* Tab Bar and title in the same row */}
<div className='w-full border-b border-gray-300 dark:border-gray-700 flex justify-between items-center bg-gray-100 dark:bg-gray-800 transition-colors duration-200'>
<TabBar activeTab={activeTab} onTabChange={handleTabChange} />
Expand Down Expand Up @@ -578,7 +578,7 @@ const App = () => {
</div>

{/* Tab content */}
<div className='tab-content bg-white dark:bg-gray-800 p-4 border-t-0 text-gray-900 dark:text-gray-100 transition-colors duration-200'>
<div className='flex-1 min-h-0 overflow-y-auto bg-white dark:bg-gray-800 p-4 border-t-0 text-gray-900 dark:text-gray-100 transition-colors duration-200'>
{activeTab === 'config' && (
<ConfigTab configContent={configContent} onConfigChange={setConfigContent} />
)}
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/components/FileTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ const FileTreeComponent = ({
};

return (
<div className='file-tree rounded-md border border-gray-200 dark:border-gray-700'>
<div className='file-tree flex min-h-0 flex-1 flex-col rounded-md border border-gray-200 dark:border-gray-700'>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find FileTree usage in SourceTab and check the structure
rg -n 'FileTree' --type=tsx -B3 -A3 src/renderer/components/SourceTab.tsx

Repository: codingworkflow/ai-code-fusion

Length of output: 100


🏁 Script executed:

# Check the specific area in FileTree.tsx around line 275
sed -n '270,280p' src/renderer/components/FileTree.tsx

Repository: codingworkflow/ai-code-fusion

Length of output: 621


🏁 Script executed:

# Check SourceTab.tsx around line 395
sed -n '390,400p' src/renderer/components/SourceTab.tsx

Repository: codingworkflow/ai-code-fusion

Length of output: 436


Remove duplicate border styling between FileTree and parent container.

The FileTree root element (line 275) has rounded-md border border-gray-200 dark:border-gray-700, and its parent container in SourceTab.tsx (line 395-399) wraps it with identical border classes rounded-md border border-gray-200 dark:border-gray-700 shadow-sm. This creates a visually doubled border.

Remove the border classes from one of the two elements—either from the FileTree root or the parent div in SourceTab.

🤖 Prompt for AI Agents
In `@src/renderer/components/FileTree.tsx` at line 275, The FileTree root div
currently applies rounded-md border border-gray-200 dark:border-gray-700 which
duplicates the identical classes applied by the parent container in SourceTab;
remove the duplicated border styling from one side—either delete the border and
rounded classes from the FileTree root div in component FileTree (the div with
className 'file-tree ...') or remove them from the parent wrapper in SourceTab
(the div that wraps FileTree and also has rounded-md border border-gray-200
dark:border-gray-700 shadow-sm) so only one element retains the border/rounded
classes to avoid the double border.

<div className='flex items-center justify-between border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 p-2'>
<div className='flex items-center'>
<input
Expand All @@ -297,7 +297,7 @@ const FileTreeComponent = ({
</span>
</div>

<div className='max-h-96 overflow-auto p-2'>
<div className='flex-1 min-h-0 overflow-auto p-2'>
{items.length === 0 ? (
<div className='flex flex-col items-center justify-center p-8 text-center text-gray-500 dark:text-gray-400'>
<svg
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/components/ProcessedTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ const ProcessedTab = ({ processedResult, onSave, onRefresh }: ProcessedTabProps)
</div>
<div
id='processed-content'
className='max-h-96 overflow-auto rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 p-4 shadow-sm'
className='max-h-[45vh] min-h-48 overflow-auto rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 p-4 shadow-sm'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using viewport height units (vh) within a scrollable flex container can lead to layout issues like double scrollbars or content being cut off. This is because vh units are relative to the viewport, not the parent container.

To align with the other responsive improvements in this PR, I recommend refactoring this component to use a flex column layout. This would make the "Processed Content" and "Files by Token Count" sections properly share the available space.

Here's an example of how you could restructure the component:

// Make the root div a flex container that fills the height
<div className='flex h-full min-h-0 flex-col'>
  {/* ...action buttons... */}

  {/* Make content sections flexible */}
  <div className='mb-4 flex flex-1 flex-col min-h-0'>
    {/* ...label... */}
    <div id='processed-content' className='flex-1 min-h-0 overflow-auto ...'>
      {/* ...pre... */}
    </div>
  </div>

  <div className='mt-6 flex flex-1 flex-col min-h-0'>
    {/* ...h3... */}
    <div className='flex-1 min-h-0 overflow-y-auto ...'>
      {/* ...table... */}
    </div>
  </div>
</div>

>
<pre className='whitespace-pre-wrap font-mono text-xs leading-5 dark:text-white'>
{processedResult.content}
Expand All @@ -211,7 +211,7 @@ const ProcessedTab = ({ processedResult, onSave, onRefresh }: ProcessedTabProps)
Files by Token Count
</h3>
<div className='rounded-md border border-gray-200 dark:border-gray-700 shadow-sm'>
<div className='h-60 max-h-60 overflow-y-auto overflow-x-hidden'>
<div className='h-[35vh] min-h-48 max-h-[45vh] overflow-y-auto overflow-x-hidden'>
<table className='min-w-full divide-y divide-gray-200'>
<thead className='sticky top-0 bg-gray-50 dark:bg-gray-800'>
<tr>
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/components/SourceTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const SourceTab = ({
}, []);

return (
<div>
<div className='flex h-full min-h-0 flex-col'>
<div className='mb-4'>
<div className='flex'>
<input
Expand Down Expand Up @@ -380,7 +380,7 @@ const SourceTab = ({
</div>

{directoryTree.length > 0 ? (
<div className='mb-6'>
<div className='mb-6 flex min-h-0 flex-1 flex-col'>
<div className='mb-2 flex items-center'>
<label
htmlFor='file-folder-selection'
Expand All @@ -392,7 +392,7 @@ const SourceTab = ({

<div
id='file-folder-selection'
className='rounded-md border border-gray-200 dark:border-gray-700 shadow-sm'
className='flex min-h-0 flex-1 rounded-md border border-gray-200 dark:border-gray-700 shadow-sm'
>
<FileTree
items={directoryTree}
Expand Down
6 changes: 4 additions & 2 deletions src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
}
</script>
</head>
<body class="bg-gray-100 dark:bg-gray-900 transition-colors duration-200">
<div id="app"></div>
<body
class="h-screen overflow-hidden bg-gray-100 dark:bg-gray-900 transition-colors duration-200"
>
<div id="app" class="h-full"></div>
<script src="bundle.js"></script>
</body>
</html>
4 changes: 0 additions & 4 deletions src/renderer/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

.tab-content {
min-height: 500px;
}

/* Folder and file icons styling */
.folder-icon,
.file-icon {
Expand Down
Loading
Loading