mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 00:21:21 +00:00
Developer Experience Improvements (multi-worktree simultaneous development) (#209)
* docs: add git hash display and configurable dev server port ## CHANGES - Add `VITE_PORT` env variable to configure dev server port - Display git commit hash in About modal next to version - Add `__GIT_HASH__` build-time constant to both Vite configs - Document running multiple Maestro instances with git worktrees - Update CONTRIBUTING.md with parallel development instructions * feat: add configurable ports for dev servers - Allow VITE_PORT to configure main dev server port - Update main window to load from configurable port - Enable VITE_WEB_PORT for web interface dev server - Add note in CONTRIBUTING.md about port configuration - Log port usage in development mode * docs: update CONTRIBUTING.md section and fix React DevTools script initialization ## CHANGES - Rename "Linting" section to "Linting & Pre-commit Hooks" in table of contents - Move script variable declaration outside conditional block - Fix React DevTools script initialization order in index.html * chore: update `.vscode/settings.json` with new markdownlint config * fix: disable biome linting. Project uses ESLint * chore: Update baseline-browser-mapping (>2 months old, warning message on "npm run build:web") * chore: add .vscode/ to gitignore * chore: fix gitignore to ignore .cscode/* files properly * fix * chore: stop tracking .vscode/ files, respect gitignore Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -33,11 +33,6 @@ scratch/
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
# .vscode/ is tracked for shared settings (settings.json, extensions.json)
|
|
||||||
# But ignore personal/local files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
@@ -55,5 +50,6 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
#VS Code
|
#VS Code
|
||||||
|
.vscode/
|
||||||
.VSCodeCounter
|
.VSCodeCounter
|
||||||
.qodo
|
.qodo
|
||||||
|
|||||||
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"editorconfig.editorconfig"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
47
.vscode/settings.json
vendored
47
.vscode/settings.json
vendored
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
// Format on save with Prettier
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
|
|
||||||
// Use tabs (matches .prettierrc and .editorconfig)
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.insertSpaces": false,
|
|
||||||
"editor.detectIndentation": false,
|
|
||||||
|
|
||||||
// ESLint configuration
|
|
||||||
"eslint.enable": true,
|
|
||||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
|
||||||
|
|
||||||
// Don't let ESLint format - let Prettier handle it
|
|
||||||
"eslint.format.enable": false,
|
|
||||||
|
|
||||||
// File-specific formatters
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[typescriptreact]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Recommended extensions
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": "explicit"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Files to exclude from search/watch
|
|
||||||
"files.exclude": {
|
|
||||||
"dist": true,
|
|
||||||
"release": true,
|
|
||||||
"node_modules": true
|
|
||||||
},
|
|
||||||
|
|
||||||
// TypeScript settings
|
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,7 @@ See [Performance Guidelines](#performance-guidelines) for specific practices.
|
|||||||
- [Project Structure](#project-structure)
|
- [Project Structure](#project-structure)
|
||||||
- [Development Scripts](#development-scripts)
|
- [Development Scripts](#development-scripts)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
- [Linting](#linting)
|
- [Linting & Pre-commit Hooks](#linting--pre-commit-hooks)
|
||||||
- [Common Development Tasks](#common-development-tasks)
|
- [Common Development Tasks](#common-development-tasks)
|
||||||
- [Adding a New AI Agent](#adding-a-new-ai-agent)
|
- [Adding a New AI Agent](#adding-a-new-ai-agent)
|
||||||
- [Code Style](#code-style)
|
- [Code Style](#code-style)
|
||||||
@@ -150,6 +150,25 @@ You can also specify a custom demo directory via environment variable:
|
|||||||
MAESTRO_DEMO_DIR=~/Desktop/my-demo npm run dev
|
MAESTRO_DEMO_DIR=~/Desktop/my-demo npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Running Multiple Instances (Git Worktrees)
|
||||||
|
|
||||||
|
When working with multiple git worktrees, you can run Maestro instances in parallel by specifying different ports using the `VITE_PORT` environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In the main worktree (uses default port 5173)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# In worktree 2 (in another directory and terminal)
|
||||||
|
VITE_PORT=5174 npm run dev
|
||||||
|
|
||||||
|
# In worktree 3
|
||||||
|
VITE_PORT=5175 npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows you to develop and test different branches simultaneously without port conflicts.
|
||||||
|
|
||||||
|
**Note:** The web interface dev server (`npm run dev:web`) uses a separate port (default 5174) and can be configured with `VITE_WEB_PORT` if needed.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run the test suite with Jest:
|
Run the test suite with Jest:
|
||||||
|
|||||||
@@ -121,3 +121,11 @@ The confirmation dialog shows the full path to the worktree directory so you kno
|
|||||||
- **Use a dedicated worktree folder** — Keep all worktrees in one place outside the main repo
|
- **Use a dedicated worktree folder** — Keep all worktrees in one place outside the main repo
|
||||||
- **Clean up when done** — Remove worktree agents after merging PRs to avoid clutter
|
- **Clean up when done** — Remove worktree agents after merging PRs to avoid clutter
|
||||||
- **Watch for Changes** — Enable file watching to keep the file tree in sync with worktree activity
|
- **Watch for Changes** — Enable file watching to keep the file tree in sync with worktree activity
|
||||||
|
- **Run multiple dev instances** — Use `VITE_PORT` environment variable to run Maestro in multiple worktrees simultaneously:
|
||||||
|
```bash
|
||||||
|
# In main worktree
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# In worktree 2 (different terminal/directory)
|
||||||
|
VITE_PORT=5174 npm run dev
|
||||||
|
```
|
||||||
|
|||||||
@@ -1111,5 +1111,4 @@ describe('AboutModal', () => {
|
|||||||
expect(screen.getByText('$12,345,678.90')).toBeInTheDocument();
|
expect(screen.getByText('$12,345,678.90')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ const mockData: StatsAggregation = {
|
|||||||
avgSessionDuration: 288000,
|
avgSessionDuration: 288000,
|
||||||
byAgentByDay: {},
|
byAgentByDay: {},
|
||||||
bySessionByDay: {},
|
bySessionByDay: {},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Empty data for edge case testing
|
// Empty data for edge case testing
|
||||||
@@ -61,7 +60,6 @@ const emptyData: StatsAggregation = {
|
|||||||
avgSessionDuration: 0,
|
avgSessionDuration: 0,
|
||||||
byAgentByDay: {},
|
byAgentByDay: {},
|
||||||
bySessionByDay: {},
|
bySessionByDay: {},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Data with large numbers
|
// Data with large numbers
|
||||||
@@ -83,7 +81,6 @@ const largeNumbersData: StatsAggregation = {
|
|||||||
avgSessionDuration: 7200000,
|
avgSessionDuration: 7200000,
|
||||||
byAgentByDay: {},
|
byAgentByDay: {},
|
||||||
bySessionByDay: {},
|
bySessionByDay: {},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Single agent data
|
// Single agent data
|
||||||
@@ -104,7 +101,6 @@ const singleAgentData: StatsAggregation = {
|
|||||||
avgSessionDuration: 360000,
|
avgSessionDuration: 360000,
|
||||||
byAgentByDay: {},
|
byAgentByDay: {},
|
||||||
bySessionByDay: {},
|
bySessionByDay: {},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only auto queries
|
// Only auto queries
|
||||||
@@ -125,7 +121,6 @@ const onlyAutoData: StatsAggregation = {
|
|||||||
avgSessionDuration: 360000,
|
avgSessionDuration: 360000,
|
||||||
byAgentByDay: {},
|
byAgentByDay: {},
|
||||||
bySessionByDay: {},
|
bySessionByDay: {},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('SummaryCards', () => {
|
describe('SummaryCards', () => {
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ const mockStatsData: StatsAggregation = {
|
|||||||
avgSessionDuration: 288000,
|
avgSessionDuration: 288000,
|
||||||
byAgentByDay: {},
|
byAgentByDay: {},
|
||||||
bySessionByDay: {},
|
bySessionByDay: {},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Chart Accessibility - AgentComparisonChart', () => {
|
describe('Chart Accessibility - AgentComparisonChart', () => {
|
||||||
@@ -411,10 +410,9 @@ describe('Chart Accessibility - General ARIA Patterns', () => {
|
|||||||
sessionsByDay: [],
|
sessionsByDay: [],
|
||||||
avgSessionDuration: 0,
|
avgSessionDuration: 0,
|
||||||
byAgentByDay: {},
|
byAgentByDay: {},
|
||||||
bySessionByDay: {},
|
bySessionByDay: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
render(<AgentComparisonChart data={emptyData} theme={mockTheme} />);
|
render(<AgentComparisonChart data={emptyData} theme={mockTheme} />);
|
||||||
expect(screen.getByText(/no agent data available/i)).toBeInTheDocument();
|
expect(screen.getByText(/no agent data available/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -212,7 +212,6 @@ const createSampleData = () => ({
|
|||||||
avgSessionDuration: 144000,
|
avgSessionDuration: 144000,
|
||||||
byAgentByDay: {},
|
byAgentByDay: {},
|
||||||
bySessionByDay: {},
|
bySessionByDay: {},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('UsageDashboard Responsive Layout', () => {
|
describe('UsageDashboard Responsive Layout', () => {
|
||||||
|
|||||||
@@ -159,10 +159,9 @@ beforeEach(() => {
|
|||||||
],
|
],
|
||||||
avgSessionDuration: 180000,
|
avgSessionDuration: 180000,
|
||||||
byAgentByDay: {},
|
byAgentByDay: {},
|
||||||
bySessionByDay: {},
|
bySessionByDay: {},
|
||||||
});
|
});
|
||||||
mockStats.getDatabaseSize.mockResolvedValue(1024 * 1024); // 1 MB
|
mockStats.getDatabaseSize.mockResolvedValue(1024 * 1024); // 1 MB
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -282,7 +281,7 @@ describe('Usage Dashboard State Transition Animations', () => {
|
|||||||
sessionsByDay: [],
|
sessionsByDay: [],
|
||||||
avgSessionDuration: 240000,
|
avgSessionDuration: 240000,
|
||||||
byAgentByDay: {},
|
byAgentByDay: {},
|
||||||
bySessionByDay: {},
|
bySessionByDay: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
it('applies dashboard-card-enter class to metric cards', () => {
|
it('applies dashboard-card-enter class to metric cards', () => {
|
||||||
@@ -589,5 +588,4 @@ describe('Usage Dashboard State Transition Animations', () => {
|
|||||||
expect(totalMaxDuration).toBeLessThan(1000);
|
expect(totalMaxDuration).toBeLessThan(1000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -147,7 +147,6 @@ const createSampleData = () => ({
|
|||||||
avgSessionDuration: 144000,
|
avgSessionDuration: 144000,
|
||||||
byAgentByDay: {},
|
byAgentByDay: {},
|
||||||
bySessionByDay: {},
|
bySessionByDay: {},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('UsageDashboardModal', () => {
|
describe('UsageDashboardModal', () => {
|
||||||
|
|||||||
@@ -642,9 +642,10 @@ function createWindow() {
|
|||||||
logger.warn(`Failed to load electron-devtools-installer: ${err.message}`, 'Window')
|
logger.warn(`Failed to load electron-devtools-installer: ${err.message}`, 'Window')
|
||||||
);
|
);
|
||||||
|
|
||||||
mainWindow.loadURL('http://localhost:5173');
|
const vitePort = process.env.VITE_PORT || '5173';
|
||||||
|
mainWindow.loadURL(`http://localhost:${vitePort}`);
|
||||||
// DevTools can be opened via Command-K menu instead of automatically on startup
|
// DevTools can be opened via Command-K menu instead of automatically on startup
|
||||||
logger.info('Loading development server', 'Window');
|
logger.info(`Loading development server on port ${vitePort}`, 'Window');
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
|
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
|
||||||
logger.info('Loading production build', 'Window');
|
logger.info('Loading production build', 'Window');
|
||||||
|
|||||||
@@ -211,8 +211,8 @@
|
|||||||
<!-- React DevTools: connects to standalone react-devtools app (npm install -g react-devtools) -->
|
<!-- React DevTools: connects to standalone react-devtools app (npm install -g react-devtools) -->
|
||||||
<!-- Only attempts connection in dev mode (Vite serves on localhost:5173) -->
|
<!-- Only attempts connection in dev mode (Vite serves on localhost:5173) -->
|
||||||
<script>
|
<script>
|
||||||
if (window.location.hostname === 'localhost') {
|
var script = document.createElement('script');
|
||||||
var script = document.createElement('script');
|
if (window.location.hostname === 'localhost') {
|
||||||
script.src = 'http://localhost:8097';
|
script.src = 'http://localhost:8097';
|
||||||
script.async = false;
|
script.async = false;
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
|||||||
@@ -11,48 +11,49 @@ const appVersion = process.env.VITE_APP_VERSION || packageJson.version;
|
|||||||
|
|
||||||
// Get the first 8 chars of git commit hash for dev mode
|
// Get the first 8 chars of git commit hash for dev mode
|
||||||
function getCommitHash(): string {
|
function getCommitHash(): string {
|
||||||
try {
|
try {
|
||||||
// Note: execSync is safe here - no user input, static git command
|
// Note: execSync is safe here - no user input, static git command
|
||||||
return execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim().slice(0, 8);
|
return execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim().slice(0, 8);
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const disableHmr = process.env.DISABLE_HMR === '1';
|
const disableHmr = process.env.DISABLE_HMR === '1';
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
plugins: [react({ fastRefresh: !disableHmr })],
|
plugins: [react({ fastRefresh: !disableHmr })],
|
||||||
root: path.join(__dirname, 'src/renderer'),
|
root: path.join(__dirname, 'src/renderer'),
|
||||||
base: './',
|
base: './',
|
||||||
define: {
|
define: {
|
||||||
__APP_VERSION__: JSON.stringify(appVersion),
|
__APP_VERSION__: JSON.stringify(appVersion),
|
||||||
// Show commit hash only in development mode
|
// Show commit hash only in development mode
|
||||||
__COMMIT_HASH__: JSON.stringify(mode === 'development' ? getCommitHash() : ''),
|
__COMMIT_HASH__: JSON.stringify(mode === 'development' ? getCommitHash() : ''),
|
||||||
// Explicitly define NODE_ENV for React and related packages
|
// Explicitly define NODE_ENV for React and related packages
|
||||||
'process.env.NODE_ENV': JSON.stringify(mode),
|
'process.env.NODE_ENV': JSON.stringify(mode),
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
// In development, use wdyr.dev.ts which loads why-did-you-render
|
// In development, use wdyr.dev.ts which loads why-did-you-render
|
||||||
// In production, use wdyr.ts which is empty (prevents bundling the library)
|
// In production, use wdyr.ts which is empty (prevents bundling the library)
|
||||||
'./wdyr': mode === 'development'
|
'./wdyr':
|
||||||
? path.join(__dirname, 'src/renderer/wdyr.dev.ts')
|
mode === 'development'
|
||||||
: path.join(__dirname, 'src/renderer/wdyr.ts'),
|
? path.join(__dirname, 'src/renderer/wdyr.dev.ts')
|
||||||
},
|
: path.join(__dirname, 'src/renderer/wdyr.ts'),
|
||||||
},
|
},
|
||||||
esbuild: {
|
},
|
||||||
// Strip console.* and debugger in production builds
|
esbuild: {
|
||||||
drop: mode === 'production' ? ['console', 'debugger'] : [],
|
// Strip console.* and debugger in production builds
|
||||||
},
|
drop: mode === 'production' ? ['console', 'debugger'] : [],
|
||||||
build: {
|
},
|
||||||
outDir: path.join(__dirname, 'dist/renderer'),
|
build: {
|
||||||
emptyOutDir: true,
|
outDir: path.join(__dirname, 'dist/renderer'),
|
||||||
},
|
emptyOutDir: true,
|
||||||
server: {
|
},
|
||||||
port: 5173,
|
server: {
|
||||||
hmr: !disableHmr,
|
port: process.env.VITE_PORT ? parseInt(process.env.VITE_PORT) : 5173,
|
||||||
// Disable file watching entirely when HMR is disabled to prevent any reloads
|
hmr: !disableHmr,
|
||||||
watch: disableHmr ? null : undefined,
|
// Disable file watching entirely when HMR is disabled to prevent any reloads
|
||||||
},
|
watch: disableHmr ? null : undefined,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { defineConfig } from 'vite';
|
|||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
|
import { execFileSync } from 'child_process';
|
||||||
|
|
||||||
// Read version from package.json
|
// Read version from package.json
|
||||||
const packageJson = JSON.parse(
|
const packageJson = JSON.parse(
|
||||||
@@ -18,6 +19,19 @@ const packageJson = JSON.parse(
|
|||||||
);
|
);
|
||||||
const appVersion = process.env.VITE_APP_VERSION || packageJson.version;
|
const appVersion = process.env.VITE_APP_VERSION || packageJson.version;
|
||||||
|
|
||||||
|
// Get git hash
|
||||||
|
function getGitHash() {
|
||||||
|
try {
|
||||||
|
return execFileSync('git', ['rev-parse', '--short=8', 'HEAD'], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
}).trim();
|
||||||
|
} catch {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const gitHash = getGitHash();
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
|
||||||
@@ -33,6 +47,7 @@ export default defineConfig(({ mode }) => ({
|
|||||||
|
|
||||||
define: {
|
define: {
|
||||||
__APP_VERSION__: JSON.stringify(appVersion),
|
__APP_VERSION__: JSON.stringify(appVersion),
|
||||||
|
__GIT_HASH__: JSON.stringify(gitHash),
|
||||||
},
|
},
|
||||||
|
|
||||||
esbuild: {
|
esbuild: {
|
||||||
@@ -128,7 +143,7 @@ export default defineConfig(({ mode }) => ({
|
|||||||
|
|
||||||
// Development server (for testing web interface standalone)
|
// Development server (for testing web interface standalone)
|
||||||
server: {
|
server: {
|
||||||
port: 5174, // Different from renderer dev server (5173)
|
port: process.env.VITE_WEB_PORT ? parseInt(process.env.VITE_WEB_PORT) : 5174, // Different from renderer dev server (5173)
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
// Proxy API calls to the running Maestro app during development
|
// Proxy API calls to the running Maestro app during development
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
Reference in New Issue
Block a user