30 Commits

Author SHA1 Message Date
Web Dev Cody
d37ced1c6e Merge pull request #841 from AutoMaker-Org/v1.0.0rc
V1.0.0rc
2026-03-15 12:58:24 -04:00
gsxdsm
0311130a8a fix: Update @github/copilot-sdk version and enhance version utility to locate package.json (#840)
- Changed @github/copilot-sdk dependency from "^0.1.16" to "0.1.16" in package.json and package-lock.json.
- Improved version utility in version.ts to check multiple candidate paths for package.json, ensuring better reliability in locating the file.
2026-03-12 19:30:17 -07:00
gsxdsm
7be8163b84 Preserve pipeline work by using waiting_approval on post-completion errors (#836)
* Changes from fix/pipeline-not-finishing

* fix: Prevent overwriting merge_conflict status in pipeline error handlers

* fix: Handle feature loading failures gracefully in error recovery

* ```
fix: Add logging when feature reload fails during status update
```
2026-03-04 20:22:44 -08:00
gsxdsm
26b73df097 Fix feature deep link with project path handling (#834)
* Changes from fix/feature-deeplink-worktree

* Update apps/ui/src/components/views/board-view.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-04 10:13:24 -08:00
gsxdsm
20e7c74b17 Fix event endpoint persistence (#831)
* Changes from fix/event-hook-endpoint

* fix: Allow empty eventHooks/ntfyEndpoints to reconcile from server

Remove the `length > 0` guards in fast-hydrate reconciliation that
prevented intentional empty-array clears from syncing across clients.
Server-side wipe protection (`__allowEmpty*` escape hatches) already
ensures empty arrays in the server are intentional.

Addresses PR #831 review feedback from CodeRabbit and Gemini.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:33:12 -08:00
gsxdsm
dd7108a7a0 Fixes critical React crash on the Kanban board view (#830)
* Changes from fix/board-react-crash

* fix: Prevent cascading re-renders and crashes from high-frequency WS events
2026-03-03 19:23:44 -08:00
gsxdsm
ae48065820 Fix dev server hang by reducing log spam and event frequency (#828)
* Changes from fix/dev-server-hang

* fix: Address PR #828 review feedback

- Reset RAF buffer on context changes (worktree switch, dev-server restart)
  to prevent stale output from flushing into new sessions
- Fix high-frequency WebSocket filter to catch auto-mode:event wrapping
  (auto_mode_progress is wrapped in auto-mode:event) and add feature:progress
- Reorder Vite aliases so explicit jsx-runtime entries aren't shadowed by
  the broad /^react(\/|$)/ regex (Vite uses first-match-wins)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: Batch dev server logs and fix React module resolution order

* feat: Add fallback timer for flushing dev server logs in background tabs

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:52:44 -08:00
gsxdsm
b2915f4de1 refactor: Simplify click URL resolution logic 2026-03-02 21:36:33 -08:00
gsxdsm
cf3d312eef fix: Remove overly restrictive pattern from summary extraction regex 2026-03-02 21:36:33 -08:00
gsxdsm
341a6534e6 refactor: extract shared isBacklogLikeStatus helper and improve comments
Address PR #825 review feedback:
- Extract duplicated backlog-like status check into shared helper in constants.ts
- Improve spec-parser regex comment to clarify subsection preservation
- Add file path reference in row-actions.tsx comment for traceability

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 21:36:33 -08:00
gsxdsm
4a128efbf4 Changes from fix/board-crash-new-feat 2026-03-02 21:36:33 -08:00
gsxdsm
54d69e907b fix: E2E test stability and UI performance improvements (#823) 2026-03-02 18:46:28 -08:00
gsxdsm
1c3d6434a8 fix: reduce excessive POST /api/auto-mode/context-exists requests
Two changes:
1. Server: Skip morgan logging for context-exists endpoint (like health check)
2. UI: Use a stable fingerprint (feature IDs + statuses) instead of the
   unstable features array reference as the useEffect dependency. This
   prevents re-checking context for all features on every React Query
   refetch when the actual feature set hasn't changed.
2026-03-02 11:14:46 -08:00
gsxdsm
8218c48e67 fix: use object destructuring in Playwright beforeEach callback
Playwright requires the first argument to use object destructuring pattern.
Changed `_` to `{}` in feature-deep-link.spec.ts beforeEach.
2026-03-02 07:50:05 -08:00
gsxdsm
59b100b5cc feat: Add conflict source branch tracking and fix auto-mode subscription cascade 2026-03-02 07:43:00 -08:00
gsxdsm
c11f390764 feat: Add conflict source branch detection and fix re-render cascade in BoardView 2026-03-02 07:20:11 -08:00
gsxdsm
33a2e04bf0 feat: Add settingsService integration for feature defaults and improve worktree handling 2026-03-02 03:28:37 -08:00
gsxdsm
34161ccc08 Changes from fix/bug-fixes-1rc 2026-03-01 21:59:02 -08:00
gsxdsm
57bcb2802d Improve auto-loop event emission and add ntfy notifications (#821) 2026-03-01 00:12:22 -08:00
gsxdsm
63b0a4fb38 Fix orphaned features when deleting worktrees (#820)
* Changes from fix/orphaned-features

* fix: Handle feature migration failures and improve UI accessibility

* feat: Add event emission for worktree deletion and feature migration

* fix: Handle OpenCode model errors and prevent duplicate model IDs

* feat: Add summary dialog and async verify with loading state

* fix: Add type attributes to buttons and improve OpenCode model selection

* fix: Add null checks for onVerify callback and opencode model selection
2026-02-28 15:42:10 -08:00
gsxdsm
1c0e460dd1 Add orphaned features management routes and UI integration (#819)
* test(copilot): add edge case test for error with code field

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Changes from fix/bug-fixes-1-0

* refactor(auto-mode): enhance orphaned feature detection and improve project initialization

- Updated detectOrphanedFeatures method to accept preloaded features, reducing redundant disk reads.
- Improved project initialization by creating required directories and files in parallel for better performance.
- Adjusted planning mode handling in UI components to clarify approval requirements for different modes.
- Added refresh functionality for file editor tabs to ensure content consistency with disk state.

These changes enhance performance, maintainability, and user experience across the application.

* feat(orphaned-features): add orphaned features management routes and UI integration

- Introduced new routes for managing orphaned features, including listing, resolving, and bulk resolving.
- Updated the UI to include an Orphaned Features section in project settings and navigation.
- Enhanced the execution service to support new orphaned feature functionalities.

These changes improve the application's capability to handle orphaned features effectively, enhancing user experience and project management.

* fix: Normalize line endings and resolve stale dirty states in file editor

* chore: Update .gitignore and enhance orphaned feature handling

- Added a blank line in .gitignore for better readability.
- Introduced a hash to worktree paths in orphaned feature resolution to prevent conflicts.
- Added validation for target branch existence during orphaned feature resolution.
- Improved prompt formatting in execution service for clarity.
- Enhanced error handling in project selector for project initialization failures.
- Refactored orphaned features section to improve state management and UI responsiveness.

These changes improve code maintainability and user experience when managing orphaned features.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-27 22:14:41 -08:00
gsxdsm
0196911d59 Bug fixes and stability improvements (#815)
* fix(copilot): correct tool.execution_complete event handling

The CopilotProvider was using incorrect event type and data structure
for tool execution completion events from the @github/copilot-sdk,
causing tool call outputs to be empty.

Changes:
- Update event type from 'tool.execution_end' to 'tool.execution_complete'
- Fix data structure to use nested result.content instead of flat result
- Fix error structure to use error.message instead of flat error
- Add success field to match SDK event structure
- Add tests for empty and missing result handling

This aligns with the official @github/copilot-sdk v0.1.16 types
defined in session-events.d.ts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(copilot): add edge case test for error with code field

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor(copilot): improve error handling and code quality

Code review improvements:
- Extract magic string '[ERROR]' to TOOL_ERROR_PREFIX constant
- Add null-safe error handling with direct error variable assignment
- Include error codes in error messages for better debugging
- Add JSDoc documentation for tool.execution_complete handler
- Update tests to verify error codes are displayed
- Add missing tool_use_id assertion in error test

These changes improve:
- Code maintainability (no magic strings)
- Debugging experience (error codes now visible)
- Type safety (explicit null checks)
- Test coverage (verify error code formatting)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Changes from fix/bug-fixes-1-0

* test(copilot): add edge case test for error with code field

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Changes from fix/bug-fixes-1-0

* fix: Handle detached HEAD state in worktree discovery and recovery

* fix: Remove unused isDevServerStarting prop and md: breakpoint classes

* fix: Add missing dependency and sanitize persisted cache data

* feat: Ensure NODE_ENV is set to test in vitest configs

* feat: Configure Playwright to run only E2E tests

* fix: Improve PR tracking and dev server lifecycle management

* feat: Add settings-based defaults for planning mode, model config, and custom providers. Fixes #816

* feat: Add worktree and branch selector to graph view

* fix: Add timeout and error handling for worktree HEAD ref resolution

* fix: use absolute icon path and place icon outside asar on Linux

The hicolor icon theme index only lists sizes up to 512x512, so an icon
installed only at 1024x1024 is invisible to GNOME/KDE's theme resolver,
causing both the app launcher and taskbar to show a generic icon.
Additionally, BrowserWindow.icon cannot be read by the window manager
when the file is inside app.asar.

- extraResources: copy logo_larger.png to resources/ (outside asar) so
  it lands at /opt/Automaker/resources/logo_larger.png on install
- linux.desktop.Icon: set to the absolute resources path, bypassing the
  hicolor theme lookup and its size constraints entirely
- icon-manager.ts: on Linux production use process.resourcesPath so
  BrowserWindow receives a real filesystem path the WM can read directly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use linux.desktop.entry for custom desktop Icon field

electron-builder v26 rejects arbitrary keys in linux.desktop — the
correct schema wraps custom .desktop overrides inside desktop.entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: set desktop name on Linux so taskbar uses the correct app icon

Without app.setDesktopName(), the window manager cannot associate the
running Electron process with automaker.desktop. GNOME/KDE fall back to
_NET_WM_ICON which defaults to Electron's own bundled icon.

Calling app.setDesktopName('automaker.desktop') before any window is
created sets the _GTK_APPLICATION_ID hint and XDG app_id so the WM
picks up the desktop entry's Icon for the taskbar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix: memory and context views mobile friendly (#818)

* Changes from fix/memory-and-context-mobile-friendly

* fix: Improve file extension detection and add path traversal protection

* refactor: Extract file extension utilities and add path traversal guards

Code review improvements:
- Extract isMarkdownFilename and isImageFilename to shared image-utils.ts
- Remove duplicated code from context-view.tsx and memory-view.tsx
- Add path traversal guard for context fixture utilities (matching memory)
- Add 7 new tests for context fixture path traversal protection
- Total 61 tests pass

Addresses code review feedback from PR #813

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: Add e2e tests for profiles crud and board background persistence

* Update apps/ui/playwright.config.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: Add robust test navigation handling and file filtering

* fix: Format NODE_OPTIONS configuration on single line

* test: Update profiles and board background persistence tests

* test: Replace iPhone 13 Pro with Pixel 5 for mobile test consistency

* Update apps/ui/src/components/views/context-view.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore: Remove test project directory

* feat: Filter context files by type and improve mobile menu visibility

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: Improve test reliability and localhost handling

* chore: Use explicit TEST_USE_EXTERNAL_BACKEND env var for server cleanup

* feat: Add E2E/CI mock mode for provider factory and auth verification

* feat: Add remoteBranch parameter to pull and rebase operations

* chore: Enhance E2E testing setup with worker isolation and auth state management

- Updated .gitignore to include worker-specific test fixtures.
- Modified e2e-tests.yml to implement test sharding for improved CI performance.
- Refactored global setup to authenticate once and save session state for reuse across tests.
- Introduced worker-isolated fixture paths to prevent conflicts during parallel test execution.
- Improved test navigation and loading handling for better reliability.
- Updated various test files to utilize new auth state management and fixture paths.

* fix: Update Playwright configuration and improve test reliability

- Increased the number of workers in Playwright configuration for better parallelism in CI environments.
- Enhanced the board background persistence test to ensure dropdown stability by waiting for the list to populate before interaction, improving test reliability.

* chore: Simplify E2E test configuration and enhance mock implementations

- Updated e2e-tests.yml to run tests in a single shard for streamlined CI execution.
- Enhanced unit tests for worktree list handling by introducing a mock for execGitCommand, improving test reliability and coverage.
- Refactored setup functions to better manage command mocks for git operations in tests.
- Improved error handling in mkdirSafe function to account for undefined stats in certain environments.

* refactor: Improve test configurations and enhance error handling

- Updated Playwright configuration to clear VITE_SERVER_URL, ensuring the frontend uses the Vite proxy and preventing cookie domain mismatches.
- Enhanced MergeRebaseDialog logic to normalize selectedBranch for better handling of various ref formats.
- Improved global setup with a more robust backend health check, throwing an error if the backend is not healthy after retries.
- Refactored project creation tests to handle file existence checks more reliably.
- Added error handling for missing E2E source fixtures to guide setup process.
- Enhanced memory navigation to handle sandbox dialog visibility more effectively.

* refactor: Enhance Git command execution and improve test configurations

- Updated Git command execution to merge environment paths correctly, ensuring proper command execution context.
- Refactored the Git initialization process to handle errors more gracefully and ensure user configuration is set before creating the initial commit.
- Improved test configurations by updating Playwright test identifiers for better clarity and consistency across different project states.
- Enhanced cleanup functions in tests to handle directory removal more robustly, preventing errors during test execution.

* fix: Resolve React hooks errors from duplicate instances in dependency tree

* style: Format alias configuration for improved readability

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: DhanushSantosh <dhanushsantoshs05@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-27 17:03:29 -08:00
gsxdsm
70d400793b Fix: memory and context views mobile friendly (#818)
* Changes from fix/memory-and-context-mobile-friendly

* fix: Improve file extension detection and add path traversal protection

* refactor: Extract file extension utilities and add path traversal guards

Code review improvements:
- Extract isMarkdownFilename and isImageFilename to shared image-utils.ts
- Remove duplicated code from context-view.tsx and memory-view.tsx
- Add path traversal guard for context fixture utilities (matching memory)
- Add 7 new tests for context fixture path traversal protection
- Total 61 tests pass

Addresses code review feedback from PR #813

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: Add e2e tests for profiles crud and board background persistence

* Update apps/ui/playwright.config.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: Add robust test navigation handling and file filtering

* fix: Format NODE_OPTIONS configuration on single line

* test: Update profiles and board background persistence tests

* test: Replace iPhone 13 Pro with Pixel 5 for mobile test consistency

* Update apps/ui/src/components/views/context-view.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore: Remove test project directory

* feat: Filter context files by type and improve mobile menu visibility

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-26 08:37:33 -08:00
DhanushSantosh
dd7654c254 fix: set desktop name on Linux so taskbar uses the correct app icon
Without app.setDesktopName(), the window manager cannot associate the
running Electron process with automaker.desktop. GNOME/KDE fall back to
_NET_WM_ICON which defaults to Electron's own bundled icon.

Calling app.setDesktopName('automaker.desktop') before any window is
created sets the _GTK_APPLICATION_ID hint and XDG app_id so the WM
picks up the desktop entry's Icon for the taskbar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 18:14:08 +05:30
DhanushSantosh
6a824a9ff0 fix: use linux.desktop.entry for custom desktop Icon field
electron-builder v26 rejects arbitrary keys in linux.desktop — the
correct schema wraps custom .desktop overrides inside desktop.entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 17:53:15 +05:30
DhanushSantosh
5e15f4120c Merge remote-tracking branch 'upstream/v1.0.0rc' into patchcraft 2026-02-26 17:49:45 +05:30
DhanushSantosh
82e9396cb8 fix: use absolute icon path and place icon outside asar on Linux
The hicolor icon theme index only lists sizes up to 512x512, so an icon
installed only at 1024x1024 is invisible to GNOME/KDE's theme resolver,
causing both the app launcher and taskbar to show a generic icon.
Additionally, BrowserWindow.icon cannot be read by the window manager
when the file is inside app.asar.

- extraResources: copy logo_larger.png to resources/ (outside asar) so
  it lands at /opt/Automaker/resources/logo_larger.png on install
- linux.desktop.Icon: set to the absolute resources path, bypassing the
  hicolor theme lookup and its size constraints entirely
- icon-manager.ts: on Linux production use process.resourcesPath so
  BrowserWindow receives a real filesystem path the WM can read directly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 17:49:26 +05:30
gsxdsm
9747faf1b9 Fix agent output summary for pipeline steps (#812)
* Changes from fix/agent-output-summary-for-pipeline-steps

* feat: Optimize pipeline summary extraction and fix regex vulnerability

* fix: Use fallback summary for pipeline steps when extraction fails

* fix: Strip follow-up session scaffold from pipeline step fallback summaries
2026-02-25 22:13:38 -08:00
DhanushSantosh
70c9fd77f6 fix: correct production icon path and refresh icon cache on RPM install
- icon-manager.ts: fix production path from '../dist/public' to '../dist'
  Vite copies public/ assets to the root of dist/, not dist/public/, so
  the old path caused electronAppExists() to return null in packaged
  builds — falling through to Electron's default icon in the taskbar
- rpm-after-install.sh: add gtk-update-icon-cache and
  update-desktop-database so GNOME/KDE picks up the installed icon and
  desktop entry immediately without a session restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:20:03 +05:30
DhanushSantosh
46ee34d499 fix: resolve desktop entry launch failure and hide Electron menu bar on Linux
- Add --ozone-platform-hint=auto before app.whenReady() so Electron
  auto-detects X11 vs Wayland when launched from a desktop entry where
  the display protocol is not guaranteed (fixes immediate crash on
  Fedora/GNOME Wayland sessions)
- Add autoHideMenuBar: true to BrowserWindow so the default Chromium
  File/Edit/View/Help menu bar is hidden by default (still accessible
  via Alt); removes visible Electron significance from the packaged app
- Add scripts/rpm-after-install.sh and wire it via afterInstall in the
  electron-builder RPM config to chmod 4755 chrome-sandbox, ensuring
  the setuid sandbox works on kernels with restricted user namespaces

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 10:53:58 +05:30
16 changed files with 452 additions and 71 deletions

1
.gitignore vendored
View File

@@ -113,3 +113,4 @@ data/
.planning/
.mcp.json
.planning
.bg-shell/

View File

@@ -32,7 +32,7 @@
"@automaker/prompts": "1.0.0",
"@automaker/types": "1.0.0",
"@automaker/utils": "1.0.0",
"@github/copilot-sdk": "^0.1.16",
"@github/copilot-sdk": "0.1.16",
"@modelcontextprotocol/sdk": "1.25.2",
"@openai/codex-sdk": "^0.98.0",
"cookie-parser": "1.4.7",

View File

@@ -2,7 +2,7 @@
* Version utility - Reads version from package.json
*/
import { readFileSync } from 'fs';
import { readFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { createLogger } from '@automaker/utils';
@@ -24,7 +24,20 @@ export function getVersion(): string {
}
try {
const packageJsonPath = join(__dirname, '..', '..', 'package.json');
const candidatePaths = [
// Development via tsx: src/lib -> project root
join(__dirname, '..', '..', 'package.json'),
// Packaged/build output: lib -> server bundle root
join(__dirname, '..', 'package.json'),
];
const packageJsonPath = candidatePaths.find((candidate) => existsSync(candidate));
if (!packageJsonPath) {
throw new Error(
`package.json not found in any expected location: ${candidatePaths.join(', ')}`
);
}
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const version = packageJson.version || '0.0.0';
cachedVersion = version;

View File

@@ -595,12 +595,12 @@ export class EventHookService {
if (clickUrl && context.projectPath) {
try {
const url = new URL(clickUrl);
url.pathname = '/board';
// Add projectPath so the UI can switch to the correct project
url.searchParams.set('projectPath', context.projectPath);
// Add featureId as query param for deep linking to board with feature output modal
if (context.featureId) {
url.pathname = '/board';
url.searchParams.set('featureId', context.featureId);
} else {
url.pathname = '/board';
}
clickUrl = url.toString();
} catch (error) {

View File

@@ -179,6 +179,7 @@ ${feature.spec}
const abortController = tempRunningFeature.abortController;
if (isAutoMode) await this.saveExecutionStateFn(projectPath);
let feature: Feature | null = null;
let pipelineCompleted = false;
try {
validateWorkingDirectory(projectPath);
@@ -431,6 +432,7 @@ Please continue from where you left off and complete all remaining tasks. Use th
testAttempts: 0,
maxTestAttempts: 5,
});
pipelineCompleted = true;
// Check if pipeline set a terminal status (e.g., merge_conflict) — don't overwrite it
const refreshed = await this.loadFeatureFn(projectPath, featureId);
if (refreshed?.status === 'merge_conflict') {
@@ -541,7 +543,30 @@ Please continue from where you left off and complete all remaining tasks. Use th
}
} else {
logger.error(`Feature ${featureId} failed:`, error);
await this.updateFeatureStatusFn(projectPath, featureId, 'backlog');
// If pipeline steps completed successfully, don't send the feature back to backlog.
// The pipeline work is done — set to waiting_approval so the user can review.
const fallbackStatus = pipelineCompleted ? 'waiting_approval' : 'backlog';
if (pipelineCompleted) {
logger.info(
`[executeFeature] Feature ${featureId} failed after pipeline completed. ` +
`Setting status to waiting_approval instead of backlog to preserve pipeline work.`
);
}
// Don't overwrite terminal states like 'merge_conflict' that were set during pipeline execution
let currentStatus: string | undefined;
try {
const currentFeature = await this.loadFeatureFn(projectPath, featureId);
currentStatus = currentFeature?.status;
} catch (loadErr) {
// If loading fails, log it and proceed with the status update anyway
logger.warn(
`[executeFeature] Failed to reload feature ${featureId} for status check:`,
loadErr
);
}
if (currentStatus !== 'merge_conflict') {
await this.updateFeatureStatusFn(projectPath, featureId, fallbackStatus);
}
this.eventBus.emitAutoModeEvent('auto_mode_error', {
featureId,
featureName: feature?.title,

View File

@@ -350,6 +350,7 @@ export class PipelineOrchestrator {
});
const abortController = runningEntry.abortController;
runningEntry.branchName = feature.branchName ?? null;
let pipelineCompleted = false;
try {
validateWorkingDirectory(projectPath);
@@ -403,6 +404,7 @@ export class PipelineOrchestrator {
};
await this.executePipeline(context);
pipelineCompleted = true;
// Re-fetch feature to check if executePipeline set a terminal status (e.g., merge_conflict)
const reloadedFeature = await this.featureStateManager.loadFeature(projectPath, featureId);
@@ -439,8 +441,21 @@ export class PipelineOrchestrator {
});
}
} else {
// If pipeline steps completed successfully, don't send the feature back to backlog.
// The pipeline work is done — set to waiting_approval so the user can review.
const fallbackStatus = pipelineCompleted ? 'waiting_approval' : 'backlog';
if (pipelineCompleted) {
logger.info(
`[resumeFromStep] Feature ${featureId} failed after pipeline completed. ` +
`Setting status to waiting_approval instead of backlog to preserve pipeline work.`
);
}
logger.error(`Pipeline resume failed for ${featureId}:`, error);
await this.updateFeatureStatusFn(projectPath, featureId, 'backlog');
// Don't overwrite terminal states like 'merge_conflict' that were set during pipeline execution
const currentFeature = await this.featureStateManager.loadFeature(projectPath, featureId);
if (currentFeature?.status !== 'merge_conflict') {
await this.updateFeatureStatusFn(projectPath, featureId, fallbackStatus);
}
this.eventBus.emitAutoModeEvent('auto_mode_error', {
featureId,
featureName: feature.title,

View File

@@ -2000,5 +2000,60 @@ describe('execution-service.ts', () => {
// The only non-in_progress status call should be absent since merge_conflict returns early
expect(statusCalls.length).toBe(0);
});
it('sets waiting_approval instead of backlog when error occurs after pipeline completes', async () => {
// Set up pipeline with steps
vi.mocked(pipelineService.getPipelineConfig).mockResolvedValue({
version: 1,
steps: [{ id: 'step-1', name: 'Step 1', order: 1, instructions: 'Do step 1' }] as any,
});
// Pipeline succeeds, but reading agent output throws after pipeline completes
mockExecutePipelineFn = vi.fn().mockResolvedValue(undefined);
// Simulate an error after pipeline completes by making loadFeature throw
// on the post-pipeline refresh call
let loadCallCount = 0;
mockLoadFeatureFn = vi.fn().mockImplementation(() => {
loadCallCount++;
if (loadCallCount === 1) return testFeature; // initial load
// Second call is the task-retry check, third is the pipeline refresh
if (loadCallCount <= 2) return testFeature;
throw new Error('Unexpected post-pipeline error');
});
const svc = createServiceWithMocks();
await svc.executeFeature('/test/project', 'feature-1');
// Should set to waiting_approval, NOT backlog, since pipeline completed
const backlogCalls = vi
.mocked(mockUpdateFeatureStatusFn)
.mock.calls.filter((call) => call[2] === 'backlog');
expect(backlogCalls.length).toBe(0);
const waitingCalls = vi
.mocked(mockUpdateFeatureStatusFn)
.mock.calls.filter((call) => call[2] === 'waiting_approval');
expect(waitingCalls.length).toBeGreaterThan(0);
});
it('still sets backlog when error occurs before pipeline completes', async () => {
// Set up pipeline with steps
vi.mocked(pipelineService.getPipelineConfig).mockResolvedValue({
version: 1,
steps: [{ id: 'step-1', name: 'Step 1', order: 1, instructions: 'Do step 1' }] as any,
});
// Pipeline itself throws (e.g., agent error during pipeline step)
mockExecutePipelineFn = vi.fn().mockRejectedValue(new Error('Agent execution failed'));
const svc = createServiceWithMocks();
await svc.executeFeature('/test/project', 'feature-1');
// Should still set to backlog since pipeline did NOT complete
const backlogCalls = vi
.mocked(mockUpdateFeatureStatusFn)
.mock.calls.filter((call) => call[2] === 'backlog');
expect(backlogCalls.length).toBe(1);
});
});
});

View File

@@ -68,7 +68,13 @@ export function NotificationBell({ projectPath }: NotificationBellProps) {
// Navigate to the relevant view based on notification type
if (notification.featureId) {
navigate({ to: '/board', search: { featureId: notification.featureId } });
navigate({
to: '/board',
search: {
featureId: notification.featureId,
projectPath: notification.projectPath || undefined,
},
});
}
},
[handleMarkAsRead, setPopoverOpen, navigate]

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useCallback, useMemo, useRef } from 'react';
import { useEffect, useState, useCallback, useMemo, useRef, startTransition } from 'react';
import { createLogger } from '@automaker/utils/logger';
import type { PointerEvent as ReactPointerEvent } from 'react';
import {
@@ -37,6 +37,7 @@ import type {
ReasoningEffort,
} from '@automaker/types';
import { pathsEqual } from '@/lib/utils';
import { initializeProject } from '@/lib/project-init';
import { toast } from 'sonner';
import {
BoardBackgroundModal,
@@ -117,9 +118,11 @@ const logger = createLogger('Board');
interface BoardViewProps {
/** Feature ID from URL parameter - if provided, opens output modal for this feature on load */
initialFeatureId?: string;
/** Project path from URL parameter - if provided, switches to this project before handling deep link */
initialProjectPath?: string;
}
export function BoardView({ initialFeatureId }: BoardViewProps) {
export function BoardView({ initialFeatureId, initialProjectPath }: BoardViewProps) {
const {
currentProject,
defaultSkipTests,
@@ -139,6 +142,7 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
setPipelineConfig,
featureTemplates,
defaultSortNewestCardOnTop,
upsertAndSetCurrentProject,
} = useAppStore(
useShallow((state) => ({
currentProject: state.currentProject,
@@ -159,6 +163,7 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
setPipelineConfig: state.setPipelineConfig,
featureTemplates: state.featureTemplates,
defaultSortNewestCardOnTop: state.defaultSortNewestCardOnTop,
upsertAndSetCurrentProject: state.upsertAndSetCurrentProject,
}))
);
// Also get keyboard shortcuts for the add feature shortcut
@@ -305,6 +310,53 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
setFeaturesWithContext,
});
// Handle deep link project switching - if URL includes a projectPath that differs from
// the current project, switch to the target project first. The feature/worktree deep link
// effect below will fire naturally once the project switch triggers a features reload.
const handledProjectPathRef = useRef<string | undefined>(undefined);
useEffect(() => {
if (!initialProjectPath || handledProjectPathRef.current === initialProjectPath) {
return;
}
// Check if we're already on the correct project
if (currentProject?.path && pathsEqual(currentProject.path, initialProjectPath)) {
handledProjectPathRef.current = initialProjectPath;
return;
}
handledProjectPathRef.current = initialProjectPath;
const switchProject = async () => {
try {
const initResult = await initializeProject(initialProjectPath);
if (!initResult.success) {
logger.warn(
`Deep link: failed to initialize project "${initialProjectPath}":`,
initResult.error
);
toast.error('Failed to open project from link', {
description: initResult.error || 'Unknown error',
});
return;
}
// Derive project name from path basename
const projectName =
initialProjectPath.split(/[/\\]/).filter(Boolean).pop() || initialProjectPath;
logger.info(`Deep link: switching to project "${projectName}" at ${initialProjectPath}`);
upsertAndSetCurrentProject(initialProjectPath, projectName);
} catch (error) {
logger.error('Deep link: project switch failed:', error);
toast.error('Failed to switch project', {
description: error instanceof Error ? error.message : 'Unknown error',
});
}
};
switchProject();
}, [initialProjectPath, currentProject?.path, upsertAndSetCurrentProject]);
// Handle initial feature ID from URL - switch to the correct worktree and open output modal
// Uses a ref to track which featureId has been handled to prevent re-opening
// when the component re-renders but initialFeatureId hasn't changed.
@@ -325,6 +377,17 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
[currentProject?.path]
)
);
// Track how many render cycles we've waited for worktrees during a deep link.
// If the Zustand store never gets populated (e.g., WorktreePanel hasn't mounted,
// useWorktrees setting is off, or the worktree query failed), we stop waiting
// after a threshold and open the modal without switching worktree.
const deepLinkRetryCountRef = useRef(0);
// Reset retry count when the feature ID changes
useEffect(() => {
deepLinkRetryCountRef.current = 0;
}, [initialFeatureId]);
useEffect(() => {
if (
!initialFeatureId ||
@@ -339,14 +402,43 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
const feature = hookFeatures.find((f) => f.id === initialFeatureId);
if (!feature) return;
// If the feature has a branch, wait for worktrees to load so we can switch
if (feature.branchName && deepLinkWorktrees.length === 0) {
return; // Worktrees not loaded yet - effect will re-run when they load
// Resolve worktrees: prefer the Zustand store (reactive), but fall back to
// the React Query cache if the store hasn't been populated yet. The store is
// only synced by the WorktreePanel's useWorktrees hook, which may not have
// rendered yet during a deep link cold start. Reading the query cache directly
// avoids an indefinite wait that hangs the app on the loading screen.
let resolvedWorktrees = deepLinkWorktrees;
if (resolvedWorktrees.length === 0 && currentProject.path) {
const cachedData = queryClient.getQueryData(queryKeys.worktrees.all(currentProject.path)) as
| { worktrees?: WorktreeInfo[] }
| undefined;
if (cachedData?.worktrees && cachedData.worktrees.length > 0) {
resolvedWorktrees = cachedData.worktrees as typeof deepLinkWorktrees;
}
}
// Switch to the correct worktree based on the feature's branchName
if (feature.branchName && deepLinkWorktrees.length > 0) {
const targetWorktree = deepLinkWorktrees.find((w) => w.branch === feature.branchName);
// If the feature has a branch and worktrees aren't available yet, wait briefly.
// After enough retries, proceed without switching worktree to avoid hanging.
const MAX_DEEP_LINK_RETRIES = 10;
if (feature.branchName && resolvedWorktrees.length === 0) {
deepLinkRetryCountRef.current++;
if (deepLinkRetryCountRef.current < MAX_DEEP_LINK_RETRIES) {
return; // Worktrees not loaded yet - effect will re-run when they load
}
// Exceeded retry limit — proceed without worktree switch to avoid hanging
logger.warn(
`Deep link: worktrees not available after ${MAX_DEEP_LINK_RETRIES} retries, ` +
`opening feature ${initialFeatureId} without switching worktree`
);
}
// Switch to the correct worktree based on the feature's branchName.
// IMPORTANT: Wrap in startTransition to batch the Zustand store update with
// any concurrent React state updates. Without this, the synchronous store
// mutation cascades through useAutoMode → refreshStatus → setAutoModeRunning,
// which can trigger React error #185 on mobile Safari/PWA crash loops.
if (feature.branchName && resolvedWorktrees.length > 0) {
const targetWorktree = resolvedWorktrees.find((w) => w.branch === feature.branchName);
if (targetWorktree) {
const currentWt = useAppStore.getState().getCurrentWorktree(currentProject.path);
const isAlreadySelected = targetWorktree.isMain
@@ -356,23 +448,27 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
logger.info(
`Deep link: switching to worktree "${targetWorktree.branch}" for feature ${initialFeatureId}`
);
setCurrentWorktree(
currentProject.path,
targetWorktree.isMain ? null : targetWorktree.path,
targetWorktree.branch
);
startTransition(() => {
setCurrentWorktree(
currentProject.path,
targetWorktree.isMain ? null : targetWorktree.path,
targetWorktree.branch
);
});
}
}
} else if (!feature.branchName && deepLinkWorktrees.length > 0) {
} else if (!feature.branchName && resolvedWorktrees.length > 0) {
// Feature has no branch - should be on the main worktree
const currentWt = useAppStore.getState().getCurrentWorktree(currentProject.path);
if (currentWt?.path !== null && currentWt !== null) {
const mainWorktree = deepLinkWorktrees.find((w) => w.isMain);
const mainWorktree = resolvedWorktrees.find((w) => w.isMain);
if (mainWorktree) {
logger.info(
`Deep link: switching to main worktree for unassigned feature ${initialFeatureId}`
);
setCurrentWorktree(currentProject.path, null, mainWorktree.branch);
startTransition(() => {
setCurrentWorktree(currentProject.path, null, mainWorktree.branch);
});
}
}
}
@@ -387,6 +483,7 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
hookFeatures,
currentProject?.path,
deepLinkWorktrees,
queryClient,
setCurrentWorktree,
setOutputFeature,
setShowOutputModal,
@@ -764,11 +861,15 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
// Recovery handler for BoardErrorBoundary: reset worktree selection to main
// so the board can re-render without the stale worktree state that caused the crash.
// Wrapped in startTransition to batch with concurrent React updates and avoid
// triggering another cascade during recovery.
const handleBoardRecover = useCallback(() => {
if (!currentProject) return;
const mainWorktree = worktrees.find((w) => w.isMain);
const mainBranch = mainWorktree?.branch || 'main';
setCurrentWorktree(currentProject.path, null, mainBranch);
startTransition(() => {
setCurrentWorktree(currentProject.path, null, mainBranch);
});
}, [currentProject, worktrees, setCurrentWorktree]);
// Helper function to add and select a worktree

View File

@@ -14,6 +14,63 @@ import { useFeature, useAgentOutput } from '@/hooks/queries';
import { queryKeys } from '@/lib/query-keys';
import { getFirstNonEmptySummary } from '@/lib/summary-selection';
import { useAppStore } from '@/store/app-store';
import { isMobileDevice } from '@/lib/mobile-detect';
// Global concurrency control for mobile mount staggering.
// When many AgentInfoPanel instances mount simultaneously (e.g., worktree switch
// with 50+ cards), we spread queries over a wider window and cap how many
// panels can be querying concurrently to prevent mobile Safari crashes.
//
// The mechanism works in two layers:
// 1. Random delay (0-6s) - spreads mount times so not all panels try to query at once
// 2. Concurrency slots (max 4) - even after the delay, only N panels can query simultaneously
//
// Instance tracking ensures the queue resets if all panels unmount (e.g., navigation).
const MOBILE_MAX_CONCURRENT_QUERIES = 4;
const MOBILE_STAGGER_WINDOW_MS = 6000; // 6s window (vs previous 2s)
let activeMobileQueryCount = 0;
let pendingMobileQueue: Array<() => void> = [];
let mountedPanelCount = 0;
function acquireMobileQuerySlot(): Promise<void> {
if (!isMobileDevice) return Promise.resolve();
if (activeMobileQueryCount < MOBILE_MAX_CONCURRENT_QUERIES) {
activeMobileQueryCount++;
return Promise.resolve();
}
return new Promise<void>((resolve) => {
pendingMobileQueue.push(() => {
activeMobileQueryCount++;
resolve();
});
});
}
function releaseMobileQuerySlot(): void {
if (!isMobileDevice) return;
activeMobileQueryCount = Math.max(0, activeMobileQueryCount - 1);
const next = pendingMobileQueue.shift();
if (next) next();
}
function trackPanelMount(): void {
if (!isMobileDevice) return;
mountedPanelCount++;
}
function trackPanelUnmount(): void {
if (!isMobileDevice) return;
mountedPanelCount = Math.max(0, mountedPanelCount - 1);
// If all panels unmounted (e.g., navigated away from board or worktree switch),
// reset the queue to prevent stale state from blocking future mounts.
if (mountedPanelCount === 0) {
activeMobileQueryCount = 0;
// Drain any pending callbacks so their Promises resolve (components already unmounted)
const pending = pendingMobileQueue;
pendingMobileQueue = [];
for (const cb of pending) cb();
}
}
/**
* Formats thinking level for compact display
@@ -66,6 +123,12 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({
const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false);
const [isTodosExpanded, setIsTodosExpanded] = useState(false);
// Track mounted panel count for global queue reset on full unmount
useEffect(() => {
trackPanelMount();
return () => trackPanelUnmount();
}, []);
// Get providers from store for provider-aware model name display
// This allows formatModelName to show provider-specific model names (e.g., "GLM 4.7" instead of "Sonnet 4.5")
// when a feature was executed using a Claude-compatible provider
@@ -92,6 +155,41 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({
// Determine if we should poll for updates
const shouldFetchData = feature.status !== 'backlog' && feature.status !== 'merge_conflict';
// On mobile, stagger initial per-card queries to prevent a mount storm.
// When a worktree loads with many cards, all AgentInfoPanel instances mount
// simultaneously. Without staggering, each card fires useFeature + useAgentOutput
// queries at the same time, creating 60-100+ concurrent API calls that crash
// mobile Safari. Actively running cards fetch immediately (priority data);
// other cards defer by a random delay AND wait for a concurrency slot.
// The stagger window is 6s (vs previous 2s) to spread load for worktrees
// with 50+ features. The concurrency limiter caps active queries to 4 at a time,
// preventing the burst that overwhelms mobile Safari's connection handling.
const [mountReady, setMountReady] = useState(!isMobileDevice || !!isActivelyRunning);
useEffect(() => {
if (mountReady) return;
let cancelled = false;
const delay = Math.random() * MOBILE_STAGGER_WINDOW_MS;
const timer = setTimeout(() => {
// After the random delay, also wait for a concurrency slot
acquireMobileQuerySlot().then(() => {
if (!cancelled) {
setMountReady(true);
// Release the slot after a brief window to let the initial queries fire
// and return, preventing all slots from being held indefinitely
setTimeout(releaseMobileQuerySlot, 3000);
} else {
releaseMobileQuerySlot();
}
});
}, delay);
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [mountReady]);
const queryEnabled = shouldFetchData && mountReady;
// Track whether we're receiving WebSocket events (within threshold)
// Use a state to trigger re-renders when the WebSocket connection becomes stale
const [isReceivingWsEvents, setIsReceivingWsEvents] = useState(false);
@@ -142,34 +240,72 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({
// Fetch fresh feature data for planSpec (store data can be stale for task progress)
const { data: freshFeature } = useFeature(projectPath, feature.id, {
enabled: shouldFetchData && !contextContent,
enabled: queryEnabled && !contextContent,
pollingInterval,
});
// Fetch agent output for parsing
const { data: agentOutputContent } = useAgentOutput(projectPath, feature.id, {
enabled: shouldFetchData && !contextContent,
enabled: queryEnabled && !contextContent,
pollingInterval,
});
// On mount, ensure feature and agent output queries are fresh.
// This handles the worktree switch scenario where cards unmount when filtered out
// and remount when the user switches back. Without this, the React Query cache
// may serve stale data (or no data) for the individual feature query, causing
// the todo list to appear empty until the next polling cycle.
// may serve stale data for the individual feature query, causing the todo list
// to appear empty until the next polling cycle.
//
// IMPORTANT: Only invalidate if the cached data EXISTS and is STALE.
// During worktree switches, ALL cards in the new worktree remount simultaneously.
// If every card fires invalidateQueries(), it creates a query storm (40-100+
// concurrent invalidations) that overwhelms React's rendering pipeline on mobile
// Safari/PWA, causing crashes. The key insight: if a query has NEVER been fetched
// (no dataUpdatedAt), there's nothing stale to invalidate — the useFeature/
// useAgentOutput hooks will fetch fresh data when their `enabled` flag is true.
// We only need to invalidate when cached data exists but is outdated.
//
// On mobile, skip mount-time invalidation entirely. The staggered useFeature/
// useAgentOutput queries already fetch fresh data — invalidation is redundant
// and creates the exact query storm we're trying to prevent. The stale threshold
// is also higher on mobile (30s vs 10s) to further reduce unnecessary refetches
// during the settling period after a worktree switch.
useEffect(() => {
if (shouldFetchData && projectPath && feature.id && !contextContent) {
// Invalidate both the single feature and agent output queries to trigger immediate refetch
queryClient.invalidateQueries({
queryKey: queryKeys.features.single(projectPath, feature.id),
});
queryClient.invalidateQueries({
queryKey: queryKeys.features.agentOutput(projectPath, feature.id),
});
if (queryEnabled && projectPath && feature.id && !contextContent) {
// On mobile, skip mount-time invalidation — the useFeature/useAgentOutput
// hooks will handle the initial fetch after the stagger delay.
if (isMobileDevice) return;
const MOUNT_STALE_THRESHOLD = 10_000; // 10s — skip invalidation if data is fresh
const now = Date.now();
const featureQuery = queryClient.getQueryState(
queryKeys.features.single(projectPath, feature.id)
);
const agentOutputQuery = queryClient.getQueryState(
queryKeys.features.agentOutput(projectPath, feature.id)
);
// Only invalidate queries that have cached data AND are stale.
// Skip if the query has never been fetched (dataUpdatedAt is undefined) —
// the useFeature/useAgentOutput hooks will handle the initial fetch.
if (featureQuery?.dataUpdatedAt && now - featureQuery.dataUpdatedAt > MOUNT_STALE_THRESHOLD) {
queryClient.invalidateQueries({
queryKey: queryKeys.features.single(projectPath, feature.id),
});
}
if (
agentOutputQuery?.dataUpdatedAt &&
now - agentOutputQuery.dataUpdatedAt > MOUNT_STALE_THRESHOLD
) {
queryClient.invalidateQueries({
queryKey: queryKeys.features.agentOutput(projectPath, feature.id),
});
}
}
// Only run on mount (feature.id and projectPath identify this specific card instance)
// Runs when mount staggering completes (queryEnabled becomes true) or on initial mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [feature.id, projectPath]);
}, [queryEnabled, feature.id, projectPath]);
// Parse agent output into agentInfo
const agentInfo = useMemo(() => {

View File

@@ -94,8 +94,14 @@ export function NotificationsView() {
// Navigate to the relevant view based on notification type
if (notification.featureId) {
// Navigate to board view with feature ID to show output
navigate({ to: '/board', search: { featureId: notification.featureId } });
// Navigate to board view with feature ID and project path to show output
navigate({
to: '/board',
search: {
featureId: notification.featureId,
projectPath: notification.projectPath || undefined,
},
});
}
},
[handleMarkAsRead, navigate]

View File

@@ -136,8 +136,14 @@ export function RecentActivityFeed({ activities, maxItems = 10 }: RecentActivity
upsertAndSetCurrentProject(projectPath, projectName);
if (activity.featureId) {
// Navigate to the specific feature
navigate({ to: '/board', search: { featureId: activity.featureId } });
// Navigate to the specific feature with project path for deep link handling
navigate({
to: '/board',
search: {
featureId: activity.featureId,
projectPath: projectPath || undefined,
},
});
} else {
navigate({ to: '/board' });
}

View File

@@ -28,33 +28,46 @@ const serverLogger = createLogger('Server');
export async function startServer(): Promise<void> {
const isDev = !app.isPackaged;
// Find Node.js executable (handles desktop launcher scenarios)
const nodeResult = findNodeExecutable({
skipSearch: isDev,
logger: (msg: string) => logger.info(msg),
});
const command = nodeResult.nodePath;
// Validate that the found Node executable actually exists
// systemPathExists is used because node-finder returns system paths
if (command !== 'node') {
let exists: boolean;
try {
exists = systemPathExists(command);
} catch (error) {
const originalError = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to verify Node.js executable at: ${command} (source: ${nodeResult.source}). Reason: ${originalError}`
);
}
if (!exists) {
throw new Error(`Node.js executable not found at: ${command} (source: ${nodeResult.source})`);
}
}
let command: string;
let commandSource: string;
let args: string[];
let serverPath: string;
if (isDev) {
// In development, run the TypeScript server via the user's Node.js.
const nodeResult = findNodeExecutable({
skipSearch: true,
logger: (msg: string) => logger.info(msg),
});
command = nodeResult.nodePath;
commandSource = nodeResult.source;
// Validate that the found Node executable actually exists
// systemPathExists is used because node-finder returns system paths
if (command !== 'node') {
let exists: boolean;
try {
exists = systemPathExists(command);
} catch (error) {
const originalError = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to verify Node.js executable at: ${command} (source: ${nodeResult.source}). Reason: ${originalError}`
);
}
if (!exists) {
throw new Error(
`Node.js executable not found at: ${command} (source: ${nodeResult.source})`
);
}
}
} else {
// In packaged builds, use Electron's bundled Node runtime instead of a system Node.
// This makes the desktop app self-contained and avoids incompatibilities with whatever
// Node version the user happens to have installed globally.
command = process.execPath;
commandSource = 'electron';
}
// __dirname is apps/ui/dist-electron (Vite bundles all into single file)
if (isDev) {
serverPath = path.join(__dirname, '../../server/src/index.ts');
@@ -133,6 +146,8 @@ export async function startServer(): Promise<void> {
PORT: state.serverPort.toString(),
DATA_DIR: dataDir,
NODE_PATH: serverNodeModules,
// Run packaged backend with Electron's embedded Node runtime.
...(app.isPackaged && { ELECTRON_RUN_AS_NODE: '1' }),
// Pass API key to server for CSRF protection
AUTOMAKER_API_KEY: state.apiKey!,
// Only set ALLOWED_ROOT_DIRECTORY if explicitly provided in environment
@@ -146,6 +161,7 @@ export async function startServer(): Promise<void> {
logger.info('[DATA_DIR_SPAWN] env.DATA_DIR=', env.DATA_DIR);
logger.info('Starting backend server...');
logger.info('Runtime command:', command, `(source: ${commandSource})`);
logger.info('Server path:', serverPath);
logger.info('Server root (cwd):', serverRoot);
logger.info('NODE_PATH:', serverNodeModules);

View File

@@ -6,6 +6,6 @@ export const Route = createLazyFileRoute('/board')({
});
function BoardRouteComponent() {
const { featureId } = useSearch({ from: '/board' });
return <BoardView initialFeatureId={featureId} />;
const { featureId, projectPath } = useSearch({ from: '/board' });
return <BoardView initialFeatureId={featureId} initialProjectPath={projectPath} />;
}

View File

@@ -4,6 +4,7 @@ import { z } from 'zod';
// Search params schema for board route
const boardSearchSchema = z.object({
featureId: z.string().optional(),
projectPath: z.string().optional(),
});
// Component is lazy-loaded via board.lazy.tsx for code splitting.

2
package-lock.json generated
View File

@@ -44,7 +44,7 @@
"@automaker/prompts": "1.0.0",
"@automaker/types": "1.0.0",
"@automaker/utils": "1.0.0",
"@github/copilot-sdk": "^0.1.16",
"@github/copilot-sdk": "0.1.16",
"@modelcontextprotocol/sdk": "1.25.2",
"@openai/codex-sdk": "^0.98.0",
"cookie-parser": "1.4.7",