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:
Kayvan Sylvan
2026-01-22 10:14:48 -08:00
committed by GitHub
parent 5778a5b34b
commit 2667cbdd77
15 changed files with 94 additions and 120 deletions

8
.gitignore vendored
View File

@@ -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

View File

@@ -1,7 +0,0 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig"
]
}

47
.vscode/settings.json vendored
View File

@@ -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
}

View File

@@ -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:

View File

@@ -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
```

View File

@@ -1111,5 +1111,4 @@ describe('AboutModal', () => {
expect(screen.getByText('$12,345,678.90')).toBeInTheDocument(); expect(screen.getByText('$12,345,678.90')).toBeInTheDocument();
}); });
}); });
}); });

View File

@@ -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', () => {

View File

@@ -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();
}); });

View File

@@ -212,7 +212,6 @@ const createSampleData = () => ({
avgSessionDuration: 144000, avgSessionDuration: 144000,
byAgentByDay: {}, byAgentByDay: {},
bySessionByDay: {}, bySessionByDay: {},
}); });
describe('UsageDashboard Responsive Layout', () => { describe('UsageDashboard Responsive Layout', () => {

View File

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

View File

@@ -147,7 +147,6 @@ const createSampleData = () => ({
avgSessionDuration: 144000, avgSessionDuration: 144000,
byAgentByDay: {}, byAgentByDay: {},
bySessionByDay: {}, bySessionByDay: {},
}); });
describe('UsageDashboardModal', () => { describe('UsageDashboardModal', () => {

View File

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

View File

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

View File

@@ -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,
},
})); }));

View File

@@ -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: {